Spaces:
Running
Running
| """Configuration pytest globale. | |
| Deux responsabilités, dans cet ordre : | |
| 1. **Ajouter le repo root à ``sys.path``** — garantit que | |
| ``tests.fixtures.*`` (mock adapters utilisés par les tests CLI | |
| E2E via dotted-path resolution ``importlib.import_module()``) | |
| sont importables de manière déterministe sur **tous les OS et | |
| versions Python**, indépendamment de la config ``pythonpath`` de | |
| pytest (qui peut diverger entre runners macOS/Windows/Linux et | |
| versions 3.11/3.12/3.13). | |
| 2. **Positionner les variables d'environnement test-friendly avant | |
| tout import de ``picarones.interfaces.web.*``** — sinon les singletons web | |
| (``JOBS_SEMAPHORE``, ``RATE_LIMITER``) seraient instanciés avec | |
| les valeurs de production au premier import, et chaque test web | |
| verrait le bocal saturé. | |
| L'isolation par-test des états globaux web (sémaphore, rate limiter, | |
| browse roots) vit dans ``tests/web/conftest.py`` — fixture | |
| ``autouse=True`` qui ne s'applique qu'aux tests sous ``tests/web/``. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| from pathlib import Path | |
| # (1) sys.path déterministe. Le repo root contient le package | |
| # ``picarones`` (déjà installable via ``pip install -e .``) ET le | |
| # package ``tests`` (importable via ``tests.fixtures.X``). On ajoute | |
| # le repo root en tête pour garantir l'import déterministe sur tous | |
| # les OS / versions Python. | |
| _REPO_ROOT = Path(__file__).resolve().parent.parent | |
| if str(_REPO_ROOT) not in sys.path: | |
| sys.path.insert(0, str(_REPO_ROOT)) | |
| # (2) Variables d'environnement. | |
| # Plafond très large pour ne jamais bloquer une suite de tests qui | |
| # démarre rapidement plusieurs benchmarks daemon en parallèle. | |
| os.environ.setdefault("PICARONES_MAX_CONCURRENT_JOBS", "32") | |
| # Mode dev par défaut. Les tests qui valident le mode public le | |
| # forcent eux-mêmes via ``monkeypatch.setenv("PICARONES_PUBLIC_MODE", "1")``. | |
| os.environ.pop("PICARONES_PUBLIC_MODE", None) | |
| # Rate limit désactivé en dev (déjà le défaut, explicité ici). | |
| os.environ.setdefault("PICARONES_RATE_LIMIT_PER_HOUR", "0") | |
| # (3) Désactivation préventive du thread daemon de tqdm. | |
| # Sur Python 3.12+ (ubuntu-latest en CI), le combo | |
| # ``tqdm._monitor`` + ``ProcessPoolExecutor`` (utilisé par | |
| # ``picarones.measurements.runner.orchestration`` pour les moteurs | |
| # CPU-bound : Tesseract, Pero OCR) provoque un hang du shutdown de | |
| # l'interpréteur après ``=== passed ===``. Le ``_python_exit`` de | |
| # ``concurrent.futures.process`` essaie de joindre les workers du | |
| # pool, mais le thread monitor de tqdm bloque la sortie globale — | |
| # le hang dépasse le timeout GNU configuré dans ci.yml (9 min) et | |
| # le job échoue avec exit code 124. | |
| # | |
| # ``monitor_interval=0`` désactive le polling thread de tqdm, qui | |
| # n'est utile qu'à l'affichage interactif des progress bars (sans | |
| # valeur ajoutée en CI où stdout est captured). Fix idiomatique | |
| # pour ce flake spécifique. | |
| try: | |
| from tqdm import tqdm as _tqdm | |
| _tqdm.monitor_interval = 0 | |
| except ImportError: # pragma: no cover | |
| # tqdm est une dep de prod (cf. pyproject.toml) ; cette branche | |
| # ne devrait jamais être atteinte en CI mais reste défensive. | |
| pass | |
| def pytest_sessionfinish(session, exitstatus) -> None: # noqa: ARG001 | |
| """Diagnostic du shutdown de l'interpréteur. | |
| Sur Python 3.12 ubuntu-latest, l'interpréteur restait jusqu'à 12 | |
| minutes en hang après ``=== passed ===`` à cause de threads | |
| non-daemon ou de connexions sqlite non fermées que les tests | |
| avaient laissés. | |
| Ce hook : | |
| 1. Liste les threads vivants à la fin de la session — si la | |
| liste contient autre chose que ``MainThread``, le développeur | |
| voit immédiatement quelle ressource fuit. | |
| 2. Force le flush stdout/stderr pour que le diagnostic apparaisse | |
| même si l'interpréteur hang ensuite. | |
| 3. Programme un ``faulthandler.dump_traceback_later(60)`` qui | |
| dumpera les stack traces de TOUS les threads après 60s | |
| d'inactivité — ce qu'on a besoin pour identifier la fuite si | |
| le hang persiste. | |
| """ | |
| import faulthandler | |
| import sys | |
| import threading | |
| alive = [ | |
| t for t in threading.enumerate() | |
| if t is not threading.main_thread() and t.is_alive() | |
| ] | |
| if alive: | |
| sys.stderr.write( | |
| "\n[conftest] threads encore vivants au sessionfinish " | |
| f"({len(alive)}) :\n", | |
| ) | |
| for t in alive: | |
| sys.stderr.write( | |
| f" - name={t.name!r} daemon={t.daemon} " | |
| f"alive={t.is_alive()}\n", | |
| ) | |
| sys.stderr.flush() | |
| # Si le shutdown hang plus de 60s, on aura les stack traces. | |
| faulthandler.dump_traceback_later( | |
| timeout=60, | |
| repeat=False, | |
| file=sys.stderr, | |
| ) | |