"""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." )