Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
"""Тесты `GET /metrics`.
Правила общей dev-БД: проверяем факт наличия метрики/дельту,
а не абсолютные значения — dev-инстанс уже содержит "живые" сеансы и
запросы, накопленные другими тестами/ручным использованием. Метрика
глубины очереди Redis проверяется на throwaway-ключе (не на реальных
`transcription`/`summarize`/`notify`/`celery`) — запись фиктивной "задачи" в
настоящую очередь рисковала бы её подхватом реальным Celery-воркером
dev-стенда.
"""
import uuid
from collections.abc import Iterable
from datetime import UTC, datetime
import httpx
import pytest
from prometheus_client.parser import text_string_to_metric_families
from prometheus_client.samples import Sample
from sqlalchemy.ext.asyncio import AsyncSession
from api import metrics as metrics_module
from core.redis import redis_client
from models.conference import Conference
from models.session import ConferenceSession
from services.conference_ids import generate_number, generate_slug
def _samples(body: str, family_name: str) -> list[Sample]:
"""Все сэмплы семейства метрик `family_name` из тела ответа `/metrics`."""
result: list[Sample] = []
for family in text_string_to_metric_families(body):
if family.name == family_name:
result.extend(family.samples)
return result
def _sample_value(samples: Iterable[Sample], *, suffix: str, **labels: str) -> float | None:
"""Значение сэмпла (`name` заканчивается на `suffix`) с точным совпадением меток."""
for sample in samples:
if sample.name.endswith(suffix) and all(
sample.labels.get(k) == v for k, v in labels.items()
):
return sample.value
return None
async def test_metrics_endpoint_returns_prometheus_exposition_format(
client: httpx.AsyncClient,
) -> None:
"""`/metrics` без авторизации отдаёт текстовый формат Prometheus с нашими метриками."""
response = await client.get("/metrics")
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/plain")
families = {family.name for family in text_string_to_metric_families(response.text)}
assert "vidconf_http_request_duration_seconds" in families
assert "vidconf_pipeline_sessions" in families
assert "vidconf_celery_queue_depth" in families
async def test_metrics_records_http_latency_by_route(client: httpx.AsyncClient) -> None:
"""Латентность известного маршрута (`GET /api/health`) попадает в histogram с этим `path`."""
await client.get("/api/health")
response = await client.get("/metrics")
samples = _samples(response.text, "vidconf_http_request_duration_seconds")
count = _sample_value(samples, suffix="_count", method="GET", path="/api/health")
assert count is not None and count >= 1
async def test_metrics_pipeline_sessions_gauge_reflects_new_session(
client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
"""Добавление сеанса в статусе `failed` увеличивает gauge для этого статуса ровно на 1."""
before_response = await client.get("/metrics")
before = _sample_value(
_samples(before_response.text, "vidconf_pipeline_sessions"),
suffix="vidconf_pipeline_sessions",
status="failed",
)
assert before is not None
conference = Conference(number=generate_number(), slug=generate_slug(), title="Metrics Test")
db_session.add(conference)
await db_session.flush()
db_session.add(
ConferenceSession(
conference_id=conference.id,
title="Metrics Test",
t_start=datetime.now(UTC),
pipeline_status="failed",
)
)
await db_session.flush()
after_response = await client.get("/metrics")
after = _sample_value(
_samples(after_response.text, "vidconf_pipeline_sessions"),
suffix="vidconf_pipeline_sessions",
status="failed",
)
assert after == before + 1
async def test_metrics_celery_queue_depth_gauge(
client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Gauge глубины очереди отражает реальный `LLEN` отслеживаемого списка Redis."""
throwaway_queue = f"test-queue-{uuid.uuid4()}"
monkeypatch.setattr(metrics_module, "CELERY_QUEUES", (throwaway_queue,))
await redis_client.rpush(throwaway_queue, "a", "b", "c")
try:
response = await client.get("/metrics")
finally:
await redis_client.delete(throwaway_queue)
value = _sample_value(
_samples(response.text, "vidconf_celery_queue_depth"),
suffix="vidconf_celery_queue_depth",
queue=throwaway_queue,
)
assert value == 3