Files
turf_saas/tests/conftest.py
DevOps Engineer 6b762068fd feat(ml): train ensemble model and generate benchmark report
Results:
  - XGBoost (Optuna 100 trials): AUC=0.7856, Precision@3=0.5783
  - LightGBM (Optuna 100 trials): AUC=0.7833, Precision@3=0.5736
  - MLP (3 layers 256-128-64): AUC=0.7743, Precision@3=0.5643
  - Ensemble (weighted voting): AUC=0.7840, Precision@3=0.5814

  Baseline XGBoost: Precision@3=0.5287
  Delta: +0.0527 (+5.3%) — DEPLOY threshold met (+5%)
  Latency: 35ms/race, 69ms/full-day (well under 200ms limit)

  SHAP: 31/43 features selected, top features: rang_cote,
  implied_prob, cote_direct, ratio_cote_field

  All 12 regression/latency tests passing.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-25 19:10:41 +02:00

125 lines
4.0 KiB
Python

"""
conftest.py — Configuration pytest globale
SaaS Turf Prédictions IA — Sprint 8 QA
Ticket: HRT-34
"""
import os
import asyncio
import pytest
from pathlib import Path
from datetime import datetime
# ============================================================
# Répertoires de sortie
# ============================================================
REPORTS_DIR = Path("tests/reports")
SCREENSHOTS_DIR = Path("tests/screenshots")
for d in [REPORTS_DIR, SCREENSHOTS_DIR]:
d.mkdir(parents=True, exist_ok=True)
# ============================================================
# Variables d'environnement
# ============================================================
BASE_URL = os.environ.get("APP_URL", "http://localhost:8792")
# ============================================================
# Fixtures globales
# ============================================================
@pytest.fixture(scope="session")
def base_url():
return BASE_URL
@pytest.fixture(scope="session")
def event_loop():
"""Event loop partagé pour les tests async de la session."""
policy = asyncio.get_event_loop_policy()
loop = policy.new_event_loop()
yield loop
loop.close()
@pytest.fixture(scope="session")
def reports_dir():
return REPORTS_DIR
@pytest.fixture(scope="session")
def screenshots_dir():
return SCREENSHOTS_DIR
# ============================================================
# Hook : screenshot automatique sur échec
# ============================================================
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Capture screenshot automatiquement sur tout test E2E en échec."""
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
# Récupérer la page Playwright si disponible dans les fixtures
page = None
for fixture_name in ("page", "context_page"):
if fixture_name in item.funcargs:
val = item.funcargs[fixture_name]
if isinstance(val, tuple):
page = val[0] # (page, browser_name)
else:
page = val
break
if page is not None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
test_name = item.name.replace("/", "_").replace(":", "_")
screenshot_path = SCREENSHOTS_DIR / f"FAIL_{test_name}_{timestamp}.png"
try:
# Playwright page.screenshot est synchrone dans les fixtures sync
# Pour les fixtures async, on force la capture
import asyncio as _asyncio
if _asyncio.iscoroutinefunction(page.screenshot):
loop = _asyncio.get_event_loop()
loop.run_until_complete(page.screenshot(path=str(screenshot_path)))
else:
page.screenshot(path=str(screenshot_path))
report.sections.append(
("Screenshot", f"Sauvegardé : {screenshot_path}")
)
except Exception as e:
report.sections.append(
("Screenshot Error", f"Impossible de capturer : {e}")
)
# ============================================================
# Marqueurs personnalisés
# ============================================================
def pytest_configure(config):
config.addinivalue_line("markers", "e2e: Tests End-to-End Playwright")
config.addinivalue_line("markers", "load: Tests de charge Locust")
config.addinivalue_line("markers", "security: Tests de sécurité")
config.addinivalue_line(
"markers", "smoke: Tests rapides de smoke (sans infra complète)"
)
config.addinivalue_line("markers", "beta: Tests spécifiques beta fermée")
config.addinivalue_line(
"markers", "requires_billing: Nécessite HRT-31 (Billing Stripe)"
)
config.addinivalue_line(
"markers", "requires_infra: Nécessite HRT-33 (infra staging)"
)