Files
anonymisation/tests/unit/test_build_specs_torch_free.py
Domi31tls e4bc9166be test(build): échec bruyant si bloc hiddenimports introuvable (revue Task 2)
- _hiddenimport_strings assert désormais si le bloc hiddenimports=[...] est
  absent (renommage/refonte de spec) au lieu de retourner [] silencieusement,
  ce qui faisait passer le test à vide.
- Docstring corrigée : la regex s'arrête au PREMIER `]` où qu'il soit (pas
  « ligne seule ») — limitation réelle documentée.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:23:51 +02:00

72 lines
3.1 KiB
Python

"""Anti-dérive P0-3 (Plan 3) : les specs frozen GUI V6 et CLI doivent être torch-free.
On vérifie le TEXTE des specs (pas d'exécution PyInstaller sous Linux) :
- aucun hiddenimport optimum*/torch*/doctr* ;
- excludes explicites présents (torch, torchvision, optimum, doctr) — ceinture
et bretelles : même si le venv de build contient torch, l'analyse l'exclut
(le core fait un `import torch` lazy dans _configure_torch_threads, que
l'analyse statique de PyInstaller suivrait sans excludes).
La spec GUI V5 legacy (anonymisation_onefile.spec) n'est PAS concernée.
"""
import re
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
TORCH_FREE_SPECS = [
ROOT / "anonymisation_gui_v6_onefile.spec",
ROOT / "anonymisation_cli_onefile.spec",
]
FORBIDDEN_HIDDEN = ("optimum", "torch", "torchvision", "doctr")
REQUIRED_EXCLUDES = ("torch", "torchvision", "optimum", "doctr")
def _hiddenimport_strings(text, spec_name):
"""Retourne les chaînes littérales présentes dans la section hiddenimports=[...].
On extrait la portion entre `hiddenimports = [` et le PREMIER `]` rencontré
où qu'il soit (`[^\\]]*` ne gère pas l'imbrication : un `]` dans un
commentaire au sein de la liste tronquerait le bloc — acceptable ici, les
specs n'en contiennent pas). Cette restriction évite les faux positifs du
bloc EXCLUDED_TORCH_STACK qui contient légitimement "torch", "optimum", etc.
Garde-fou : si le bloc hiddenimports est introuvable (renommage, refonte de
la spec), on échoue BRUYAMMENT au lieu de retourner [] — sinon le test
passerait à vide sans plus rien vérifier.
"""
match = re.search(r"hiddenimports\s*=\s*\[([^\]]*)\]", text, re.DOTALL)
assert match is not None, f"bloc hiddenimports introuvable dans {spec_name}"
block = match.group(1)
return re.findall(r"[\"']([A-Za-z0-9_.]+)[\"']", block)
@pytest.mark.parametrize("spec_path", TORCH_FREE_SPECS, ids=lambda p: p.name)
def test_spec_sans_hiddenimport_torch_optimum_doctr(spec_path):
text = spec_path.read_text(encoding="utf-8")
hits = [
s for s in _hiddenimport_strings(text, spec_path.name)
if s.split(".")[0] in FORBIDDEN_HIDDEN
]
assert hits == [], f"{spec_path.name} référence encore : {hits}"
@pytest.mark.parametrize("spec_path", TORCH_FREE_SPECS, ids=lambda p: p.name)
def test_spec_declare_excludes_torch(spec_path):
text = spec_path.read_text(encoding="utf-8")
assert "excludes=EXCLUDED_TORCH_STACK" in text, (
f"{spec_path.name} : Analysis() sans excludes=EXCLUDED_TORCH_STACK"
)
for name in REQUIRED_EXCLUDES:
assert f'"{name}"' in text.split("EXCLUDED_TORCH_STACK")[1].split("]")[0], (
f"{spec_path.name} : '{name}' absent de EXCLUDED_TORCH_STACK"
)
def test_spec_legacy_v5_garde_optimum():
text = (ROOT / "anonymisation_onefile.spec").read_text(encoding="utf-8")
assert '"optimum"' in text, (
"anonymisation_onefile.spec (GUI V5 legacy) doit GARDER optimum — "
"si ce test casse, quelqu'un a modifié la spec legacy par erreur."
)