Тесты на queued_s/duration_s фиксируют ровно ту путаницу, из-за которой метрика и разъехалась: ожидание в очереди и время работы — разные величины, а у прогонов до миграции 006 длительности просто нет (вместо неё раньше показывалось время в очереди). Панель отладки теперь показывает, сколько идущий прогон уже работает — по этому и виден застрявший, а не только по отсутствию heartbeat. README: фактические числа тестов (150, проверено прогоном run_tests.sh). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
4.0 KiB
Python
87 lines
4.0 KiB
Python
"""Юнит-тесты шкалы загрузки и таймингов прогонов — чистая логика, без БД."""
|
|
|
|
from datetime import datetime
|
|
|
|
from app.core.progress import run_percent
|
|
from app.schemas.admin import ParseRunResponse
|
|
|
|
|
|
def test_queued_run_shows_nothing_done():
|
|
assert run_percent("queued", "queued", target=100, fetched=0, processed=0) == 0.0
|
|
|
|
|
|
def test_fetch_stage_fills_first_half():
|
|
assert run_percent("running", "fetch", target=100, fetched=50, processed=0) == 25.0
|
|
assert run_percent("running", "fetch", target=100, fetched=100, processed=0) == 50.0
|
|
|
|
|
|
def test_index_stage_fills_second_half():
|
|
# Выборка закончена (50%), проиндексирована половина полученного → 75%
|
|
assert run_percent("running", "index", target=100, fetched=100, processed=50) == 75.0
|
|
assert run_percent("running", "index", target=100, fetched=100, processed=100) == 100.0
|
|
|
|
|
|
def test_partial_fetch_does_not_inflate_index_progress():
|
|
"""Источник отдал меньше лимита: выборка даёт свои 20%, индексация — свои."""
|
|
percent = run_percent("running", "index", target=100, fetched=40, processed=20)
|
|
assert percent == 45.0 # 20% за выборку + 25% за половину индексации
|
|
|
|
|
|
def test_unknown_target_gives_zero_instead_of_division_error():
|
|
assert run_percent("running", "fetch", target=0, fetched=17, processed=0) == 0.0
|
|
assert run_percent("running", "index", target=0, fetched=0, processed=0) == 0.0
|
|
|
|
|
|
def test_overshoot_is_clamped():
|
|
"""Источник может вернуть больше лимита — шкала не должна уезжать за 100%."""
|
|
assert run_percent("running", "fetch", target=10, fetched=99, processed=0) == 50.0
|
|
assert run_percent("running", "index", target=10, fetched=10, processed=99) == 100.0
|
|
|
|
|
|
def test_finished_runs_are_always_full():
|
|
"""Шкала показывает «работа окончена», исход виден по статусу рядом."""
|
|
for status in ("done", "partial", "error", "cancelled"):
|
|
assert run_percent(status, "finished", target=100, fetched=3, processed=1) == 100.0
|
|
|
|
|
|
# ─── Тайминги прогона в схеме ответа ─────────────────────────────────────────
|
|
# Регрессия, ради которой они и разделены: в отладке «длительность прогона»
|
|
# показывала время ожидания в очереди (часы) вместо времени работы (секунды).
|
|
|
|
|
|
def _run(**over) -> ParseRunResponse:
|
|
base = {
|
|
"id": 1, "source_id": 1, "status": "done", "stage": "finished", "target": 100,
|
|
"fetched": 100, "processed": 100, "added": 10, "duplicates": 90,
|
|
"skipped": 0, "failed": 0,
|
|
"started_at": datetime(2026, 8, 27, 12, 0, 0),
|
|
"run_started_at": datetime(2026, 8, 27, 14, 0, 0),
|
|
"finished_at": datetime(2026, 8, 27, 14, 0, 50),
|
|
}
|
|
base.update(over)
|
|
return ParseRunResponse(**base)
|
|
|
|
|
|
def test_queued_and_duration_are_measured_separately():
|
|
r = _run()
|
|
assert r.queued_s == 7200.0 # два часа в очереди
|
|
assert r.duration_s == 50.0 # полминуты работы
|
|
|
|
|
|
def test_no_durations_until_worker_picked_run_up():
|
|
r = _run(status="queued", stage="queued", run_started_at=None, finished_at=None)
|
|
assert r.queued_s is None
|
|
assert r.duration_s is None
|
|
|
|
|
|
def test_running_run_has_wait_but_no_duration_yet():
|
|
r = _run(status="running", stage="index", finished_at=None)
|
|
assert r.queued_s == 7200.0
|
|
assert r.duration_s is None
|
|
|
|
|
|
def test_legacy_runs_report_no_duration_instead_of_queue_time():
|
|
"""Прогоны до миграции 006: длительности нет — но и вранья тоже."""
|
|
r = _run(run_started_at=None)
|
|
assert r.duration_s is None
|