Picarones / tests /architecture /test_reports_layer_strict.py
Claude
refactor(audit): Phase 3.1 + Phase 5 — code mort robustness + violations archi
1d1017e unverified
Raw
History Blame
4.66 kB
"""Phase 5.1 audit code-quality — ``picarones.reports/`` (couche 7)
n'importe que depuis les couches **strictement intérieures** : domain,
formats, evaluation, pipeline.
Avant la Phase 5.1, 4 imports illégaux ``reports/ → app/`` (couche
7 → 6) violaient l'orientation des couches :
- ``reports/csv/render.py:53`` → ``from picarones.app.results import RunResult``
- ``reports/json/render.py:51`` → idem
- ``reports/html/render.py:54`` → ``from picarones.app.results import RunDocumentResult, RunResult``
La résolution a déplacé ``RunResult`` / ``RunDocumentResult`` /
``ReportRenderer`` de ``picarones.app.results`` vers
``picarones.pipeline.run_result`` (couche 4) — accessible depuis
``reports/`` selon l'ordre canonique du manifeste CLAUDE.md.
Ce test verrouille la règle pour empêcher la régression : tout PR
qui réintroduit un ``from picarones.app...`` dans ``reports/``
échoue ici.
"""
from __future__ import annotations
import ast
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
REPORTS_DIR = REPO_ROOT / "picarones" / "reports"
#: Couches strictement intérieures à ``reports/`` (couche 7). Les
#: imports cross-package du module ``reports`` ne doivent pointer
#: que vers ces noms.
ALLOWED_INNER_LAYERS: frozenset[str] = frozenset({
"domain", # 1
"formats", # 2
"evaluation", # 3
"pipeline", # 4
})
#: Couches **extérieures** à ``reports/`` — interdites en import.
FORBIDDEN_OUTER_LAYERS: frozenset[str] = frozenset({
"adapters", # 5
"app", # 6
# 7 = reports lui-même (auto-import OK)
"interfaces", # 8
})
def _imported_top_packages(path: Path) -> set[str]:
"""Retourne l'ensemble des sous-paquets de ``picarones`` importés
par un fichier ``reports/...``. Exclut les sous-modules de
``reports`` lui-même."""
tree = ast.parse(path.read_text(encoding="utf-8"))
out: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
mod = node.module or ""
parts = mod.split(".")
if len(parts) >= 2 and parts[0] == "picarones":
out.add(parts[1])
elif isinstance(node, ast.Import):
for alias in node.names:
parts = alias.name.split(".")
if len(parts) >= 2 and parts[0] == "picarones":
out.add(parts[1])
return out
def test_reports_does_not_import_from_outer_layers() -> None:
"""``picarones/reports/**.py`` ne contient aucun import depuis
une couche plus externe (``adapters``, ``app``, ``interfaces``).
"""
offenders: list[tuple[str, set[str]]] = []
for path in sorted(REPORTS_DIR.rglob("*.py")):
if "__pycache__" in path.parts:
continue
imported = _imported_top_packages(path)
bad = imported & FORBIDDEN_OUTER_LAYERS
if bad:
offenders.append((str(path.relative_to(REPO_ROOT)), bad))
if offenders:
lines = "\n".join(
f" {p} importe depuis : {sorted(b)}"
for p, b in offenders
)
raise AssertionError(
"Imports illégaux ``reports/ → couches externes`` :\n"
+ lines
+ "\n\nLa règle CLAUDE.md interdit à ``reports/`` (couche 7) "
"d'importer depuis ``adapters/`` (5), ``app/`` (6) ou "
"``interfaces/`` (8). Si le type recherché vit dans ces "
"couches, soit le déplacer vers ``pipeline/`` (couche 4) "
"ou plus interne, soit créer un protocole côté ``domain/``."
)
def test_run_result_moved_to_pipeline_layer() -> None:
"""``RunResult`` / ``RunDocumentResult`` / ``ReportRenderer``
doivent être importables depuis ``picarones.pipeline.run_result``.
Régression : si un PR repasse les définitions canoniques dans
``app/results.py``, le shim de re-export plante.
"""
from picarones.pipeline.run_result import (
ReportRenderer,
RunDocumentResult,
RunResult,
)
assert RunResult is not None
assert RunDocumentResult is not None
assert ReportRenderer is not None
def test_app_results_is_compat_shim() -> None:
"""``picarones.app.results`` reste accessible (compat interne)
et expose les mêmes noms que ``picarones.pipeline.run_result``."""
from picarones.app import results as app_results
from picarones.pipeline import run_result as canonical
assert app_results.RunResult is canonical.RunResult
assert app_results.RunDocumentResult is canonical.RunDocumentResult
assert app_results.ReportRenderer is canonical.ReportRenderer