salvirezwan's picture
End-to-end real-time traffic analytics pipeline β€” YOLOv8s ONNX
5b60216
Raw
History Blame Contribute Delete
5.8 kB
"""API integration tests β€” no ONNX model or GPU required.
Uses httpx AsyncClient with FastAPI's TestClient.
The pipeline is never actually started in these tests; we test the
REST scaffolding and schema validation only.
Run with:
pytest tests/test_api.py -v
"""
from __future__ import annotations
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
# Patch database to use a temp file so tests don't touch the real DB
import tempfile, api.database as _db_module
from pathlib import Path
_tmp_db = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
_tmp_db.close()
_db_module.DB_PATH = Path(_tmp_db.name)
from api.main import app
@pytest_asyncio.fixture
async def client():
from api.database import init_db
await init_db() # ensure tables exist before each test
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
yield c
# ── Health ────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_health(client: AsyncClient):
r = await client.get("/health")
assert r.status_code == 200
body = r.json()
assert body["status"] == "ok"
assert "timestamp" in body
# ── Root ──────────────────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_root(client: AsyncClient):
r = await client.get("/")
assert r.status_code == 200
assert "TrafficVision" in r.json()["message"]
# ── Pipeline status (not running) ─────────────────────────────────────────────
@pytest.mark.asyncio
async def test_pipeline_status_not_running(client: AsyncClient):
r = await client.get("/pipeline/status")
assert r.status_code == 200
body = r.json()
assert body["running"] is False
assert body["frame_index"] == 0
# ── Pipeline start validation ─────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_pipeline_start_invalid_conf(client: AsyncClient):
"""confidence_threshold > 1.0 should be rejected by Pydantic."""
r = await client.post("/pipeline/start", json={"source": "0", "confidence_threshold": 1.5})
assert r.status_code == 422
@pytest.mark.asyncio
async def test_pipeline_start_invalid_jpeg_quality(client: AsyncClient):
r = await client.post("/pipeline/start", json={"source": "0", "jpeg_quality": 0})
assert r.status_code == 422
# ── Analytics: recent (empty ring buffer) ────────────────────────────────────
@pytest.mark.asyncio
async def test_analytics_recent_empty(client: AsyncClient):
r = await client.get("/analytics/recent")
assert r.status_code == 200
body = r.json()
assert body["frames"] == []
assert body["window_seconds"] == 300
@pytest.mark.asyncio
async def test_analytics_recent_custom_window(client: AsyncClient):
r = await client.get("/analytics/recent?window=60")
assert r.status_code == 200
assert r.json()["window_seconds"] == 60
@pytest.mark.asyncio
async def test_analytics_recent_window_too_small(client: AsyncClient):
r = await client.get("/analytics/recent?window=5")
assert r.status_code == 422
# ── Analytics: hourly (empty DB) ─────────────────────────────────────────────
@pytest.mark.asyncio
async def test_analytics_hourly_empty(client: AsyncClient):
r = await client.get("/analytics/hourly")
assert r.status_code == 200
assert r.json() == []
# ── Analytics: summary (no data) ─────────────────────────────────────────────
@pytest.mark.asyncio
async def test_analytics_summary_no_data(client: AsyncClient):
r = await client.get("/analytics/summary")
assert r.status_code == 404
# ── Schemas: MetricsMessage round-trip ───────────────────────────────────────
def test_metrics_message_schema():
from api.schemas import MetricsMessage
import time
payload = {
"timestamp": time.time(),
"frame_index": 42,
"total_count": 10,
"count_per_class": {"car": 8, "bus": 2},
"vehicles_in_frame": 3,
"avg_speed_kmh": 45.0,
"speed_samples": [40.0, 45.0, 50.0],
"alerts": [],
"tracks": [
{"id": 1, "bbox": [0.0, 0.0, 100.0, 100.0],
"class_id": 0, "class_name": "car", "confidence": 0.9, "speed_kmh": None}
],
}
msg = MetricsMessage(**payload)
assert msg.frame_index == 42
assert msg.tracks[0].class_name == "car"
# Round-trip via JSON
restored = MetricsMessage.model_validate_json(msg.model_dump_json())
assert restored.total_count == 10
def test_pipeline_start_request_coerces_source():
from api.schemas import PipelineStartRequest
req = PipelineStartRequest(source="2")
assert req.source == 2 # int coercion
def test_counting_line_as_spec():
from api.schemas import CountingLineConfig
line = CountingLineConfig(x1=0, y1=100, x2=1280, y2=100)
spec = line.as_spec()
assert spec.p1 == (0, 100)
assert spec.p2 == (1280, 100)