Picarones / tests /architecture /test_file_budgets.py
Claude
feat(sprint-H.4)!: drop _legacy/ prefix — interfaces/{cli,web} consolidées
de9192c unverified
Raw
History Blame
13.5 kB
"""Garde-fou contre la croissance silencieuse des fichiers.
Chaque fichier listé dans :data:`FILE_BUDGETS` a un budget en lignes.
Si un fichier dépasse son budget, le test échoue et la PR est forcée
à choisir entre :
1. **Refactor** pour rentrer dans le budget (extraire un sous-module,
factoriser, supprimer du code mort).
2. **Relever le budget délibérément** : modifier la valeur dans ce
fichier en l'expliquant dans le message de commit. La hausse devient
un acte conscient, plus une dérive silencieuse.
Calibration : snapshot v1.0.0 (2026-05-02), ``current + ~15 %`` de marge
pour l'évolution naturelle. Les god-modules historiques (statistics,
generator, runner) gardent un budget proche de leur taille actuelle ; le
choix de les dégonfler est une décision dédiée à un sprint de refactor,
pas un sous-produit de l'invariant.
Re-calibrer à chaque release tag.
"""
from __future__ import annotations
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
# Format : chemin relatif → max_lines.
# Seuls les fichiers ≥ 400 lignes sont surveillés (les petits fichiers
# n'ont pas besoin de budget — leur croissance est gérée par les tests
# de couverture, pas par un seuil dur).
FILE_BUDGETS: dict[str, int] = {
# Sprint D.1 (plan v2.0) — adapter de compat run_benchmark legacy
# → BenchmarkService rewrite. Module qui présente l'API
# historique mono-call ``run_benchmark(corpus, engines, ...)``
# consommée par les interfaces CLI/web.
# Sprint D.2.b a ajouté ~260 LOC pour la branche resumable.
# Sprint D.2.c-f a ajouté ~190 LOC : NER attach + over_normalization
# + validate_profile.
# Sprint H.2.c a retiré ``_ocr_only_to_spec`` legacy + simplifié
# ``build_adapter_resolver`` (canonique uniquement).
# Sprint H.4 — module renommé ``_legacy_runner_adapter`` →
# ``benchmark_runner`` (drop le préfixe legacy : c'est l'entry
# point canonique des interfaces vers ``BenchmarkService``).
"picarones/app/services/benchmark_runner.py": 1700, # actuel ~1450
# --- God-modules : budget actuel + 15 % de marge.
# Le rétrécissement sera l'objet d'un sprint de refactor dédié.
# statistics.py (1128 lignes) a été éclaté en sous-package
# ``picarones/measurements/statistics/`` lors du sprint
# « découpage de statistics.py » (2026-05-02). Plus aucun fichier
# de la famille ne dépasse 350 lignes, donc aucune entrée requise.
# runner.py (1019 lignes) a été éclaté en sous-package
# ``picarones/measurements/runner/`` lors du sprint
# « découpage de runner.py » (2026-05-03). Le sous-package a été
# supprimé en Sprint D.6.b du plan v2.0 — son entrée dans
# ``FILE_BUDGETS`` a été retirée.
# --- Refactor (sprint « découpage de generator.py ») : passé de
# 1063 à 431 lignes via extraction vers picarones/report/assets.py
# et le sous-package picarones/report/report_data/. Budget serré
# à 500 pour verrouiller le gain ; toute croissance > 500 sera
# un signal pour redécouper.
# Phase 5.E : ``report/generator.py`` est désormais un shim ;
# canonique dans ``reports/html/generator.py``.
"picarones/reports/html/generator.py": 550, # actuel 471
# --- Fichiers métier larges.
# (Phase 7.D — ``reports/html/renderers/pipeline.py`` supprimé.)
# Phase 4-ter : ``core/results.py`` est désormais un shim
# (≤ 25 l). Le contenu canonique vit dans ``evaluation/`` ;
# même budget pour la même raison historique (modèles
# BenchmarkResult/EngineReport/DocumentResult).
"picarones/evaluation/benchmark_result.py": 750, # actuel 702
# Phase 5.C : ``report/philological_render.py`` est désormais
# un shim (≤ 25 l). Le contenu canonique vit dans
# ``reports/html/renderers/philological.py``.
"picarones/reports/html/renderers/philological.py": 700, # actuel 601
# Sprint E.1 du plan v2.0 — module migré vers ``evaluation/metrics/``.
"picarones/evaluation/metrics/modern_archives.py": 700, # actuel 599
# Sprint E.4 du plan v2.0 — migré vers ``evaluation/metrics/``.
"picarones/evaluation/metrics/builtin_hooks.py": 700, # actuel 590
# Sprint E.5 du plan v2.0 — modules ``history`` et ``robustness``
# migrés depuis ``measurements/`` vers la couche canonique.
"picarones/evaluation/metrics/history.py": 720, # actuel 615
"picarones/evaluation/metrics/robustness.py": 850, # actuel 742
# (Phase 7.D — ``pipeline/legacy_runner.py`` et
# ``pipeline/legacy_pipeline_benchmark.py`` supprimés.)
# Phase 8 — importers IIIF/Gallica déplacés vers ``adapters/corpus/``.
"picarones/adapters/corpus/iiif.py": 675, # actuel 567
"picarones/adapters/corpus/gallica.py": 675, # actuel 563
# Sprint A14-S10 + Lot D — déplacés depuis measurements/.
# L'ancien emplacement (shim) a été supprimé au Lot D ; seul le
# canonique reste dans evaluation/metrics/.
"picarones/evaluation/metrics/levers.py": 675, # actuel 561
"picarones/evaluation/metrics/inter_engine.py": 575, # actuel 484
"picarones/adapters/corpus/escriptorium.py": 650, # actuel 553 (Phase 8)
# Sprint A14-S1 — A.I.0 P0 : ajout de validated_path,
# validated_prompt_filename, safe_report_name et compute_workspace_roots.
# Ces helpers seront extraits dans ``picarones/web/path_security.py``
# lors du Sprint S20 du rewrite ciblé (création couche app/services/).
# Sprint F du plan v2.0 — déplacé vers ``interfaces/web/``.
"picarones/interfaces/web/security.py": 850, # actuel 751
# Sprint A14-S8 — CorpusRunner introduit pour orchestrer les
# pipelines composées sur un corpus avec backpressure / timeout
# réel / annulation propre. Budget stable, l'extension
# ProcessPoolExecutor (S11) restera dans cette enveloppe.
"picarones/pipeline/runner.py": 550, # actuel 462
# Sprint A14-S28 — PipelineExecutor refondu pour consommer un
# ExecutionPlan (run_plan) tout en gardant run(spec) comme sucre.
# PipelinePlanner introduit pour transformer une PipelineSpec en
# plan immuable (validation + bindings + jonctions de métriques).
# Sprint A14-S47 — branchement ArtifactStore : +60 lignes (lookup
# cache avant exec, persistance après succès, helpers privés).
"picarones/pipeline/executor.py": 600, # actuel 541
"picarones/pipeline/planner.py": 465, # actuel 403
# Sprint A14-S29 — ArtifactStore (ABC + 2 implémentations) avec
# hash multi-paramètres pour adresser la critique d'audit n° 14
# « hash multi-paramètres + reprise par hash ».
"picarones/adapters/storage/artifact_store.py": 580, # actuel 504
# Sprint A14-S37 + S52 + S56 — JobStore SQLite : POST/GET/DELETE,
# JobStoreError, schema_version table (S56) + busy_timeout 30s +
# WAL mode pour les jobs concurrents.
"picarones/adapters/storage/job_store.py": 500, # actuel 421
# Sprint A14-S41 — artifacts_index.jsonl séparé.
"picarones/app/services/benchmark_service.py": 470, # actuel 400
# Sprint A14-S44 — BaseLLMAdapter implémente le contrat StepExecutor
# (input_types, output_types, execute) en plus de complete().
# S59 ajout du descripteur ``_DeprecatedAttribute`` + alias rétrocompat
# ``DEFAULT_CORRECTION_PROMPT`` + warning lang fallback (M6).
"picarones/adapters/llm/base.py": 560, # actuel 486
# Phase 4-quater : ``core/corpus.py`` est désormais un shim
# (≤ 30 l). Le contenu canonique vit dans ``evaluation/`` ;
# même budget pour la même raison historique
# (Document/Corpus/GTLevel + 5 payloads + load_corpus_from_directory).
"picarones/evaluation/corpus.py": 600, # actuel 533
# Sprint H.1 du plan v2.0 — ``fixtures.py`` migré vers
# ``evaluation/synthetic.py``.
"picarones/evaluation/synthetic.py": 600, # actuel 510
# Phase 5.C.batch7 + Lot D : le shim
# ``measurements/roman_numerals.py`` a été supprimé. Seul le
# canonique ``evaluation/metrics/roman_numerals.py`` reste.
"picarones/evaluation/metrics/roman_numerals.py": 575, # actuel 484
# Sprint A14-S11 + Lot I — déplacés depuis extras/importers/.
# Les shims ``extras/importers/{htr_united, huggingface,
# _fallback_log}`` ont été supprimés au Lot I (mai 2026).
"picarones/adapters/corpus/htr_united.py": 575, # actuel 473
"picarones/adapters/corpus/huggingface.py": 550, # actuel 464
# Sprint G du plan v2.0 — déplacé vers ``interfaces/cli/``.
"picarones/interfaces/cli/_workflows.py": 550, # actuel 469
# ``__init__.py`` du legacy CLI — plus gros que les autres car il
# contient les commandes ``info``, ``engines``, ``metrics``,
# ``report``, ``demo``.
"picarones/interfaces/cli/__init__.py": 500, # actuel 396
# Phase 4-ter : ``core/metric_hooks.py`` est désormais un shim
# (≤ 80 l). Le contenu canonique vit dans ``evaluation/`` ;
# même budget pour la même raison historique (centralise les
# hooks document/corpus, croissance maîtrisée).
"picarones/evaluation/metric_hooks.py": 500, # actuel 427
# Phase 5.C.batch7 : ``measurements/numerical_sequences.py`` est
# désormais un shim ; canonique dans
# ``evaluation/metrics/numerical_sequences.py``.
"picarones/evaluation/metrics/numerical_sequences.py": 500, # actuel 428
# Sprint A14-S9 + Lot D — déplacé depuis measurements/normalization.py.
# Le shim a été supprimé au Lot D ; seul le canonique reste.
"picarones/formats/text/normalization.py": 500, # actuel 420
# Phase 5.E : ``report/comparison.py`` est désormais un shim ;
# canonique dans ``reports/html/comparison.py``.
"picarones/reports/html/comparison.py": 500, # actuel 414
# --- Module mutualisé créé par le sprint des render helpers
# (Sprint « consolidation des renderers » 2026-05-02). Budget
# calibré sur la taille post-documentation des conventions.
# Phase 5 : ``report/render_helpers.py`` est désormais un shim
# (≤ 25 l). Le contenu canonique vit dans
# ``reports/_helpers/`` ; même budget pour la même raison
# historique (consolidation des 25 helpers de couleur).
"picarones/reports/_helpers/render_helpers.py": 480, # actuel 428
# --- Services applicatifs et orchestration du rewrite ciblé.
# Budgets calibrés à current + 15 % de marge. La CLI elle-même
# reste mince (~110 lignes) — toute logique métier vit dans
# ``app/services/``.
"picarones/app/services/corpus_service.py": 625, # actuel 541
"picarones/app/services/path_security.py": 470, # actuel 410
"picarones/app/services/run_orchestrator.py": 500, # actuel 432
# Le rendu HTML vit en couche ``reports/`` (cible documentée
# du rewrite — un rapport est un format de sortie, pas un
# service métier).
"picarones/reports/html/render.py": 700, # actuel 615
}
def _line_count(path: Path) -> int:
"""Compte les lignes physiques (y compris vides)."""
return len(path.read_text(encoding="utf-8").splitlines())
@pytest.mark.parametrize(
("rel_path", "budget"),
sorted(FILE_BUDGETS.items()),
)
def test_file_size_within_budget(rel_path: str, budget: int) -> None:
"""Chaque fichier surveillé doit rester ≤ budget."""
path = REPO_ROOT / rel_path
assert path.exists(), (
f"Fichier disparu : {rel_path}. "
"Retire l'entrée de FILE_BUDGETS dans "
"tests/architecture/test_file_budgets.py."
)
actual = _line_count(path)
assert actual <= budget, (
f"\n{rel_path} a {actual} lignes (budget {budget}).\n\n"
"Soit refactor pour rentrer dans le budget, soit relève le budget "
"consciemment dans tests/architecture/test_file_budgets.py "
"avec une justification dans le message de commit."
)
def test_no_orphaned_budget_entries() -> None:
"""Toute entrée de FILE_BUDGETS doit pointer vers un fichier existant."""
missing = [p for p in FILE_BUDGETS if not (REPO_ROOT / p).exists()]
assert not missing, (
f"Entrées orphelines dans FILE_BUDGETS : {missing}. "
"Le fichier a été déplacé/supprimé — retire l'entrée."
)
def test_budget_table_covers_all_large_files() -> None:
"""Tout fichier ≥ 400 lignes doit avoir une entrée dans FILE_BUDGETS.
Empêche un fichier nouveau ou subitement gros d'échapper à la
surveillance. Si un fichier dépasse 400 lignes, ajoute-le à
FILE_BUDGETS avec son budget (current + 15 %).
"""
threshold = 400
untracked: list[tuple[str, int]] = []
for path in (REPO_ROOT / "picarones").rglob("*.py"):
rel = path.relative_to(REPO_ROOT).as_posix()
if rel in FILE_BUDGETS:
continue
count = _line_count(path)
if count >= threshold:
untracked.append((rel, count))
assert not untracked, (
f"\nFichiers ≥ {threshold} lignes non surveillés :\n"
+ "\n".join(f" {p} ({n} lignes)" for p, n in sorted(untracked))
+ "\n\nAjoute-les à FILE_BUDGETS dans "
"tests/architecture/test_file_budgets.py avec budget = current + ~15 %."
)