"""API /api/tests — sessioni di test su training esistente (max 2 utenti simultanei).""" from __future__ import annotations import json import time import uuid from typing import Optional import httpx from fastapi import APIRouter, Depends, HTTPException from core import api_client, db, minio_client from core.auth import require_auth from core.docker_runner import run_test_once router = APIRouter(prefix="/api/tests", tags=["tests"]) def _row(r): if r is None: return None d = dict(r) for k in ("started_at", "ended_at"): if d.get(k) is not None and hasattr(d[k], "isoformat"): d[k] = d[k].isoformat() return d @router.post("/sessions", status_code=201) async def start_session(body: dict, user=Depends(require_auth)): training_id = body.get("training_id") if not training_id: raise HTTPException(400, "training_id required") tr = await db.fetchrow( "SELECT id, status FROM trainings WHERE id = $1", uuid.UUID(training_id) ) if not tr: raise HTTPException(404, "training not found") if tr["status"] != "succeeded": raise HTTPException(409, "training not completed") sid = str(uuid.uuid4()) try: await api_client.page_connect("test", user.get("username") or "unknown", sid) except httpx.HTTPStatusError as e: if e.response.status_code == 429: raise HTTPException(429, "test slots full (max 2 users)") raise HTTPException(502, f"api: {e}") row = await db.fetchrow( "INSERT INTO tests (id, training_id, user_id) VALUES ($1,$2,$3) RETURNING *", uuid.UUID(sid), uuid.UUID(training_id), user.get("username") or "unknown", ) return _row(row) @router.post("/sessions/{session_id}/ping") async def ping_session(session_id: str, user=Depends(require_auth)): try: await api_client.page_ping(session_id) except httpx.HTTPStatusError as e: raise HTTPException(e.response.status_code, e.response.text) return {"ok": True} @router.post("/sessions/{session_id}/runs", status_code=201) async def run_test(session_id: str, body: dict, user=Depends(require_auth)): row = await db.fetchrow("SELECT * FROM tests WHERE id = $1", uuid.UUID(session_id)) if not row: raise HTTPException(404, "session not found") inputs = body.get("inputs") or {} t0 = time.monotonic() try: result = await run_test_once(str(row["training_id"]), inputs) except Exception as e: raise HTTPException(500, f"test run failed: {e}") dt_ms = int((time.monotonic() - t0) * 1000) run = { "inputs": inputs, "outputs": result.get("outputs", {}), "duration_ms": dt_ms, "cpu_peak": result.get("cpu_peak"), "mem_peak_mb": result.get("mem_peak_mb"), "ts": time.time(), } await db.execute( "UPDATE tests SET runs = runs || $1::jsonb WHERE id = $2", json.dumps([run]), uuid.UUID(session_id), ) return run @router.delete("/sessions/{session_id}", status_code=204) async def end_session(session_id: str, user=Depends(require_auth)): await db.execute( "UPDATE tests SET ended_at = NOW() WHERE id = $1 AND ended_at IS NULL", uuid.UUID(session_id), ) try: await api_client.page_disconnect(session_id) except Exception: pass return None