Spaces:
Running
test: corriger 4 défauts de classification du chantier B
Browse filesL'audit a identifié que la classification automatique du commit B2
avait mal placé 23 fichiers dans des sous-dossiers qui ne reflètent
pas le sujet réel du test.
**1. 13 tests ``tests/core/`` → ``tests/measurements/``**
L'algorithme ``Counter.most_common()`` privilégiait l'ordre
alphabétique en cas d'égalité ; comme les tests métriques importaient
fréquemment 1-2 modules ``picarones.core.*`` (``metric_registry``,
``modules``) en plus du module ``picarones.measurements.X`` testé,
ils étaient classés en ``core/``.
Tests concernés : ``test_sprint{38,52,53,55,56,57,58,59,60,64,65,84,85}_*``
— tous testent un module sous ``picarones.measurements/``.
**2. 6 tests ``_html`` ``tests/measurements/`` → ``tests/report/``**
Les tests dont le nom comporte le suffixe ``_html`` testent
explicitement le **rendu HTML** d'une métrique (renderer dans
``picarones.report.*_render``). Ils croisent ``measurements`` et
``report`` mais leur sujet principal est le rendu :
``test_sprint{46_stratification,67_pipeline,68_pipeline_comparison,86_aii5,87_readability,88_robustness_projection}_html``.
**3. 4 tests ambigus reclassés**
- ``test_sprint69_user_doc.py`` (était en ``core/``) → ``integration/``
(test du contenu de ``docs/user/`` — pas une abstraction cercle 1).
- ``test_sprint13_parallelisation_stats.py`` (``measurements/`` →
``integration/``) — teste le runner parallèle via 5 adapters OCR
simultanément, c'est un test E2E.
- ``test_sprint7_advanced_report.py`` (``measurements/`` → ``report/``)
— teste ``ReportGenerator``, pas une métrique.
- ``test_sprint28_ux_save_compare.py`` (``report/`` → ``web/``) —
teste les endpoints ``/api/config/save``, ``/api/config/load``,
``/api/benchmark/{job_id}/synthesis_preview``, etc. C'est un test
HTTP, pas un test de rendu.
**4. ``conftest.py`` racine scindé**
Avant : ``tests/conftest.py`` exposait une fixture
``autouse=True _isolate_web_app_state`` qui s'appliquait à **tous**
les tests, même ceux des cercles 1 et 2 qui n'utilisaient jamais
``picarones.web.*``. Conséquences :
- Chaque test ``tests/core/test_corpus.py`` chargeait inutilement
``picarones.web.app`` à chaque exécution (plusieurs ms d'I/O
d'import × 3354 tests = pollution mesurable).
- L'isolation web mutait l'état global même quand non pertinent.
Après :
- ``tests/conftest.py`` réduit à ~30 lignes — ne fait plus que
positionner les ``os.environ.setdefault`` AVANT le premier import
de ``picarones.web.*``. C'est une opération à exécuter une fois,
pas par-test.
- ``tests/web/conftest.py`` (nouveau) — contient la fixture
``autouse=True``, automatiquement scopée aux tests sous
``tests/web/``. Le ``try/except ImportError`` est supprimé
puisqu'on est garanti que les imports ``picarones.web.*``
réussissent dans ce contexte.
Répartition finale
------------------
tests/core/ 10 fichiers (-13)
tests/measurements/ 49 fichiers (-3 ; -6 _html ; +13 from core)
tests/engines/ 9 fichiers (inchangé)
tests/report/ 21 fichiers (+1 sprint7 ; +6 _html ; -1 sprint28)
tests/web/ 5 fichiers (+1 sprint28)
tests/extras/ 1 fichier (inchangé)
tests/cli/ 1 fichier (inchangé)
tests/integration/ 12 fichiers (+2 ambigus)
Pytest : 3354 passed, 2 skipped, 0 failed (inchangé).
Ruff : All checks passed.
https://claude.ai/code/session_01Hsd7kL8yeCbXn1mA7GQK9L
- tests/conftest.py +19 -77
- tests/{measurements → integration}/test_sprint13_parallelisation_stats.py +0 -0
- tests/{core → integration}/test_sprint69_user_doc.py +0 -0
- tests/{core → measurements}/test_sprint38_ner_metrics.py +0 -0
- tests/{core → measurements}/test_sprint52_readability.py +0 -0
- tests/{core → measurements}/test_sprint53_reading_order.py +0 -0
- tests/{core → measurements}/test_sprint55_unicode_blocks.py +0 -0
- tests/{core → measurements}/test_sprint56_abbreviations.py +0 -0
- tests/{core → measurements}/test_sprint57_mufi.py +0 -0
- tests/{core → measurements}/test_sprint58_early_modern.py +0 -0
- tests/{core → measurements}/test_sprint59_modern_archives.py +0 -0
- tests/{core → measurements}/test_sprint60_roman_numerals.py +0 -0
- tests/{core → measurements}/test_sprint64_pipeline_benchmark.py +0 -0
- tests/{core → measurements}/test_sprint65_pipeline_comparison.py +0 -0
- tests/{core → measurements}/test_sprint84_searchability.py +0 -0
- tests/{core → measurements}/test_sprint85_numerical_sequences.py +0 -0
- tests/{measurements → report}/test_sprint46_stratification_html.py +0 -0
- tests/{measurements → report}/test_sprint67_pipeline_html.py +0 -0
- tests/{measurements → report}/test_sprint68_pipeline_comparison_html.py +0 -0
- tests/{measurements → report}/test_sprint7_advanced_report.py +0 -0
- tests/{measurements → report}/test_sprint86_aii5_html.py +0 -0
- tests/{measurements → report}/test_sprint87_readability_html.py +0 -0
- tests/{measurements → report}/test_sprint88_robustness_projection_html.py +0 -0
- tests/web/conftest.py +66 -0
- tests/{report → web}/test_sprint28_ux_save_compare.py +0 -0
|
@@ -1,88 +1,30 @@
|
|
| 1 |
"""Configuration pytest globale.
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
créé avec la valeur prod (2) au moment où le premier test
|
| 16 |
-
l'utilise.
|
| 17 |
-
2. Restaure l'état entre chaque test via une fixture autouse qui
|
| 18 |
-
purge le rate limiter, ré-injecte un sémaphore frais, et recalcule
|
| 19 |
-
les browse roots selon l'environnement courant.
|
| 20 |
"""
|
| 21 |
|
| 22 |
from __future__ import annotations
|
| 23 |
|
| 24 |
import os
|
| 25 |
-
import threading
|
| 26 |
-
|
| 27 |
-
import pytest
|
| 28 |
-
|
| 29 |
|
| 30 |
-
#
|
| 31 |
-
#
|
| 32 |
-
# ---------------------------------------------------------------------------
|
| 33 |
-
|
| 34 |
-
# Plafond très large pour ne jamais bloquer une suite de tests qui démarre
|
| 35 |
-
# rapidement plusieurs benchmarks daemon (Sprint 6 + Sprint 24).
|
| 36 |
os.environ.setdefault("PICARONES_MAX_CONCURRENT_JOBS", "32")
|
| 37 |
-
# S'assurer qu'on est en mode dev pour les tests existants ; les tests
|
| 38 |
-
# Sprint 24 qui valident le mode public le forcent eux-mêmes via monkeypatch.
|
| 39 |
-
os.environ.pop("PICARONES_PUBLIC_MODE", None)
|
| 40 |
-
# Rate limit désactivé en dev (déjà le cas, mais explicite).
|
| 41 |
-
os.environ.setdefault("PICARONES_RATE_LIMIT_PER_HOUR", "0")
|
| 42 |
-
|
| 43 |
|
| 44 |
-
#
|
| 45 |
-
#
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
@pytest.fixture(autouse=True)
|
| 49 |
-
def _isolate_web_app_state():
|
| 50 |
-
"""Réinitialise sémaphore, rate limiter et browse roots entre tests.
|
| 51 |
-
|
| 52 |
-
Sans cette fixture :
|
| 53 |
-
- une suite séquentielle de 32+ tests qui font ``POST /api/benchmark/*``
|
| 54 |
-
peut épuiser le sémaphore (les threads daemon ne libèrent qu'à la
|
| 55 |
-
fin du benchmark Tesseract, qui prend une fraction de seconde mais
|
| 56 |
-
plusieurs tests consécutifs se bousculent) ;
|
| 57 |
-
- un test Sprint 24 qui mute ``_BROWSE_ROOTS`` localement laisse une
|
| 58 |
-
liste pointant vers un ``tmp_path`` purgé ;
|
| 59 |
-
- les tests qui appellent le rate limiter directement laissent des
|
| 60 |
-
timestamps dans son bucket.
|
| 61 |
-
"""
|
| 62 |
-
try:
|
| 63 |
-
from picarones.web import app as web_app
|
| 64 |
-
from picarones.web import security as web_sec
|
| 65 |
-
from picarones.web import state as web_state
|
| 66 |
-
from picarones.web.routers import corpus as web_corpus_router
|
| 67 |
-
except ImportError:
|
| 68 |
-
# Tests non-web : aucun état à restaurer.
|
| 69 |
-
yield
|
| 70 |
-
return
|
| 71 |
-
|
| 72 |
-
# Sauvegarde
|
| 73 |
-
original_browse_roots = list(web_corpus_router._BROWSE_ROOTS)
|
| 74 |
-
|
| 75 |
-
# Sémaphore frais à chaque test (capacité large, voir conftest top-level).
|
| 76 |
-
# Le module ``state`` détient le singleton ; ``app`` en a juste une
|
| 77 |
-
# référence (importée comme ``_JOBS_SEMAPHORE``) — on synchronise les deux.
|
| 78 |
-
new_sem = threading.Semaphore(web_sec.get_max_concurrent_jobs())
|
| 79 |
-
web_state.JOBS_SEMAPHORE = new_sem
|
| 80 |
-
web_app._JOBS_SEMAPHORE = new_sem
|
| 81 |
-
web_state.RATE_LIMITER.reset()
|
| 82 |
-
web_state.RATE_LIMITER.max_per_hour = web_sec.get_rate_limit_per_hour()
|
| 83 |
-
|
| 84 |
-
yield
|
| 85 |
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
web_state.RATE_LIMITER.reset()
|
|
|
|
| 1 |
"""Configuration pytest globale.
|
| 2 |
|
| 3 |
+
Ce conftest racine ne fait **qu'une seule chose** : positionner les
|
| 4 |
+
variables d'environnement test-friendly **avant** tout import de
|
| 5 |
+
``picarones.web.*``. Sans ça, les singletons web (``JOBS_SEMAPHORE``,
|
| 6 |
+
``RATE_LIMITER``) seraient instanciés avec les valeurs de production
|
| 7 |
+
(2 jobs concurrents max, rate limit selon mode public) au moment du
|
| 8 |
+
premier import, et chaque test web verrait le bocal saturé.
|
| 9 |
+
|
| 10 |
+
L'isolation par-test des états globaux web (sémaphore, rate limiter,
|
| 11 |
+
browse roots) vit dans ``tests/web/conftest.py`` — fixture
|
| 12 |
+
``autouse=True`` qui ne s'applique qu'aux tests sous ``tests/web/``,
|
| 13 |
+
pour éviter qu'un test cercle 1 (``tests/core/``) ne paie le coût
|
| 14 |
+
de l'import de ``picarones.web.*`` à chaque exécution.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
|
| 17 |
from __future__ import annotations
|
| 18 |
|
| 19 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
# Plafond très large pour ne jamais bloquer une suite de tests qui
|
| 22 |
+
# démarre rapidement plusieurs benchmarks daemon en parallèle.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
os.environ.setdefault("PICARONES_MAX_CONCURRENT_JOBS", "32")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
# Mode dev par défaut. Les tests qui valident le mode public le
|
| 26 |
+
# forcent eux-mêmes via ``monkeypatch.setenv("PICARONES_PUBLIC_MODE", "1")``.
|
| 27 |
+
os.environ.pop("PICARONES_PUBLIC_MODE", None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
# Rate limit désactivé en dev (déjà le défaut, explicité ici).
|
| 30 |
+
os.environ.setdefault("PICARONES_RATE_LIMIT_PER_HOUR", "0")
|
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fixtures locales aux tests de l'interface web FastAPI.
|
| 2 |
+
|
| 3 |
+
Le serveur Picarones expose dans :mod:`picarones.web.state` trois
|
| 4 |
+
états globaux partagés entre routes :
|
| 5 |
+
|
| 6 |
+
- ``JOBS_SEMAPHORE`` — sémaphore borné pour les benchmarks concurrents,
|
| 7 |
+
- ``RATE_LIMITER`` — rate limiter par IP,
|
| 8 |
+
- ``picarones.web.routers.corpus._BROWSE_ROOTS`` — répertoires
|
| 9 |
+
autorisés à la navigation, calculés au chargement.
|
| 10 |
+
|
| 11 |
+
Ces états peuvent polluer des tests indépendants. Cette fixture
|
| 12 |
+
réinitialise les trois entre chaque test web — purge le rate
|
| 13 |
+
limiter, ré-injecte un sémaphore frais, recalcule les browse roots
|
| 14 |
+
selon l'environnement courant.
|
| 15 |
+
|
| 16 |
+
Discipline : la fixture est ``autouse=True`` mais n'est définie
|
| 17 |
+
que dans ``tests/web/conftest.py`` — les tests des cercles 1 et 2
|
| 18 |
+
(``tests/core/``, ``tests/measurements/``, etc.) ne paient pas le
|
| 19 |
+
coût de l'import de ``picarones.web.*`` à chaque test.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import threading
|
| 25 |
+
|
| 26 |
+
import pytest
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@pytest.fixture(autouse=True)
|
| 30 |
+
def _isolate_web_app_state():
|
| 31 |
+
"""Réinitialise sémaphore, rate limiter et browse roots entre tests.
|
| 32 |
+
|
| 33 |
+
Sans cette fixture :
|
| 34 |
+
|
| 35 |
+
- une suite séquentielle de 32+ tests qui font ``POST /api/benchmark/*``
|
| 36 |
+
peut épuiser le sémaphore (les threads daemon ne libèrent qu'à la
|
| 37 |
+
fin du benchmark Tesseract, qui prend une fraction de seconde mais
|
| 38 |
+
plusieurs tests consécutifs se bousculent) ;
|
| 39 |
+
- un test qui mute ``_BROWSE_ROOTS`` localement laisse une liste
|
| 40 |
+
pointant vers un ``tmp_path`` purgé ;
|
| 41 |
+
- les tests qui appellent le rate limiter directement laissent des
|
| 42 |
+
timestamps dans son bucket.
|
| 43 |
+
"""
|
| 44 |
+
from picarones.web import app as web_app
|
| 45 |
+
from picarones.web import security as web_sec
|
| 46 |
+
from picarones.web import state as web_state
|
| 47 |
+
from picarones.web.routers import corpus as web_corpus_router
|
| 48 |
+
|
| 49 |
+
# Sauvegarde
|
| 50 |
+
original_browse_roots = list(web_corpus_router._BROWSE_ROOTS)
|
| 51 |
+
|
| 52 |
+
# Sémaphore frais à chaque test (capacité large, voir les env vars
|
| 53 |
+
# par défaut dans ``tests/conftest.py``). Le module ``state``
|
| 54 |
+
# détient le singleton ; ``app`` en a une référence importée comme
|
| 55 |
+
# ``_JOBS_SEMAPHORE`` — on synchronise les deux.
|
| 56 |
+
new_sem = threading.Semaphore(web_sec.get_max_concurrent_jobs())
|
| 57 |
+
web_state.JOBS_SEMAPHORE = new_sem
|
| 58 |
+
web_app._JOBS_SEMAPHORE = new_sem
|
| 59 |
+
web_state.RATE_LIMITER.reset()
|
| 60 |
+
web_state.RATE_LIMITER.max_per_hour = web_sec.get_rate_limit_per_hour()
|
| 61 |
+
|
| 62 |
+
yield
|
| 63 |
+
|
| 64 |
+
# Restauration
|
| 65 |
+
web_corpus_router._BROWSE_ROOTS[:] = original_browse_roots
|
| 66 |
+
web_state.RATE_LIMITER.reset()
|
|
File without changes
|