Spaces:
Sleeping
Sleeping
File size: 7,444 Bytes
3bffe86 2f5797b 3bffe86 2f5797b 3bffe86 2f5797b 3bffe86 2f5797b 3bffe86 0a7509a 3bffe86 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | """Tests de cohérence du ``SPECS.md`` avec le code réel.
Sprint A2 (préparation des gates pour B-12 — refonte SPECS en A14).
Le SPECS actuel (mars 2025 + addendum sprints 16-30) est désynchronisé
d'~75 sprints. La refonte intégrale est planifiée en Sprint A14. En
attendant, on pose ici un **garde-fou minimal** qui évite que de
nouvelles divergences ne s'ajoutent silencieusement :
1. Le document doit exister et déclarer une version + date.
2. Toute promesse explicitement *abandonnée* depuis SPECS v1 doit être
marquée par une balise ``Reporté`` ou ``Abandonné`` (pour qu'un
primo-lecteur ne s'attende pas à trouver la fonctionnalité).
Le test est délibérément permissif : il ne vérifie pas le contenu
fonctionnel section par section (c'est le rôle de A14). Il garantit
seulement qu'on ne reculera pas davantage.
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
SPECS_PATH = REPO_ROOT / "docs" / "reference" / "specification.md"
def _read_specs() -> str:
if not SPECS_PATH.exists():
pytest.skip("specification.md absent")
return SPECS_PATH.read_text(encoding="utf-8")
# ---------------------------------------------------------------------------
# Existence et meta
# ---------------------------------------------------------------------------
def test_specs_exists() -> None:
"""Pré-requis : la spec doit exister (Phase 1 D5 : déplacée de
SPECS.md (racine) vers docs/reference/specification.md)."""
assert SPECS_PATH.exists(), (
f"{SPECS_PATH.relative_to(REPO_ROOT)} absent. Si retiré "
"volontairement, supprimer aussi ce test."
)
def test_specs_declares_version_and_date() -> None:
"""SPECS doit déclarer son numéro de version et sa date (ligne ``Version`` ou ``Date``)."""
text = _read_specs()
has_version = bool(re.search(r"\bVersion\s*\d", text, re.IGNORECASE))
has_date = bool(
re.search(r"\b(?:janvier|février|mars|avril|mai|juin|juillet|août|septembre|octobre|novembre|décembre)\s+\d{4}", text, re.IGNORECASE)
or re.search(r"\b(?:january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{4}", text, re.IGNORECASE)
)
assert has_version and has_date, (
"SPECS.md doit contenir une ligne 'Version X.Y' et un mois en lettres "
"(ex: 'Mars 2025'). Manque : "
f"version={has_version}, date={has_date}."
)
# ---------------------------------------------------------------------------
# Promesses abandonnées doivent être marquées
# ---------------------------------------------------------------------------
#: Items que SPECS v1 promettait mais qui ne sont pas implémentés
#: au 2 mai 2026. Pour chacun, le SPECS doit soit (a) ne plus en parler,
#: soit (b) l'accompagner d'une balise « Reporté », « Abandonné », « Annulé »
#: ou « Non implémenté » dans un rayon de 200 caractères.
#:
#: Note historique : Kraken et Calamari étaient initialement listés
#: ici (promesses SPECS v1 abandonnées), puis ré-implémentés en
#: Phase 3 du chantier post-rewrite (mai 2026). Ils sont aujourd'hui
#: des adapters de premier rang (cf. ``picarones/adapters/ocr/``)
#: et leur mention dans spec.md § 4.2 n'a plus besoin d'être encadrée
#: par un marqueur d'abandon.
ABANDONED_FEATURES_TO_FLAG = {
"AWS Textract": ["AWS Textract"],
"OCRopus": ["OCRopus"],
"Recommandation automatique": [
"Recommandation automatique",
"recommandation automatique : quel concurrent",
],
"Export PDF": ["Export PDF", "PDF synthétique"],
"k-means clustering": ["k-means", "Clustering automatique des patterns"],
"Annotations inline": ["Annotations inline"],
"Badge SVG qualité": ["Badge de qualité générable", "SVG quality badge"],
}
#: Mots-clés qui marquent un statut « non livré ».
DEPRECATION_MARKERS = (
"reporté",
"reportée",
"abandonné",
"abandonnée",
"annulé",
"annulée",
"non implémenté",
"non implémentée",
"non livré",
"non livrée",
"deferred",
"abandoned",
"cancelled",
"not implemented",
)
#: Balises HTML qui ouvrent et ferment un bloc de promesses
#: explicitement déclarées comme abandonnées. Les features listées
#: entre ces deux balises sont acceptées par le test, où qu'elles
#: apparaissent dans le document.
ABANDONED_BLOCK_START = "<!-- specs-check: known-abandoned-start -->"
ABANDONED_BLOCK_END = "<!-- specs-check: known-abandoned-end -->"
def _extract_abandoned_block(text: str) -> str:
"""Retourne le contenu du bloc d'abandon déclaré, ou chaîne vide."""
start = text.find(ABANDONED_BLOCK_START)
end = text.find(ABANDONED_BLOCK_END)
if start == -1 or end == -1 or end < start:
return ""
return text[start + len(ABANDONED_BLOCK_START) : end].lower()
def _has_deprecation_nearby(text: str, idx: int, window: int = 200) -> bool:
"""Vrai si l'un des marqueurs est présent dans une fenêtre autour de ``idx``."""
start = max(0, idx - window)
end = min(len(text), idx + window)
snippet = text[start:end].lower()
return any(marker in snippet for marker in DEPRECATION_MARKERS)
def _is_globally_abandoned(text: str, pattern: str) -> bool:
"""Vrai si ``pattern`` est listé dans le bloc d'abandon global ET
accompagné d'un marqueur de deprecation dans le bloc."""
block = _extract_abandoned_block(text)
if not block:
return False
if pattern.lower() not in block:
return False
# Le bloc lui-même doit contenir au moins un marqueur de deprecation
return any(marker in block for marker in DEPRECATION_MARKERS)
@pytest.mark.parametrize("feature_name,patterns", list(ABANDONED_FEATURES_TO_FLAG.items()))
def test_abandoned_feature_marked_or_absent(feature_name: str, patterns: list[str]) -> None:
"""Pour chaque promesse abandonnée, l'une des trois conditions
suivantes doit être satisfaite :
1. La feature n'apparaît plus dans SPECS ;
2. Chaque mention est accompagnée d'un marqueur de deprecation
dans une fenêtre de 200 chars ;
3. La feature est listée dans un bloc global
``<!-- specs-check: known-abandoned-start -->`` … ``-end -->``
qui contient lui-même un marqueur de deprecation.
La condition 3 permet de centraliser la documentation des
abandons dans un encart unique sans devoir paraphraser à chaque
occurrence."""
text = _read_specs()
text_lower = text.lower()
# Condition 3 : bloc global
for pattern in patterns:
if _is_globally_abandoned(text, pattern):
return
unmarked: list[tuple[str, int]] = []
for pattern in patterns:
for m in re.finditer(re.escape(pattern.lower()), text_lower):
if not _has_deprecation_nearby(text, m.start()):
unmarked.append((pattern, m.start()))
assert not unmarked, (
f"Mention(s) de '{feature_name}' dans SPECS.md sans balise de "
f"deprecation à proximité (mots tolérés : {DEPRECATION_MARKERS}). "
f"Positions : {unmarked}. "
f"Soit retirer la mention, soit ajouter une note explicite "
f"« — reporté / abandonné / non livré », soit lister la feature "
f"dans le bloc <!-- specs-check: known-abandoned-start --> en tête de SPECS.md."
)
|