feat(gui): GUI V6 G3 — câblage moteur, Configuration, licence UI, build-prep
G3-A câblage moteur réel (engine_bridge.py) : EngineSettings + NerManagers à chargement paresseux (aucun manager à l'import), kwargs alignés CLI/V5 (make_vector_redaction=False, also_make_raster_burn=True, config_path, use_hf, ner/gliner/camembert_manager, ogc_label) ; make_process_fn engine injectable ; état managers not_loaded/loading/ready/unavailable, échecs optionnels tolérés. G3-B Configuration (config_state.py + tabs/tab_config.py) : ConfigState → EngineSettings, profils via profile_defaults (path injectable), options raster/NER local/profil/sortie, état managers, sections admin-only via admin_mode. G3-C Licence UI (machine_id.py + tab_about) : activation par clef (LicenseClient.activate), bouton vérifier (check), affichage statut, aucun token loggé, aucun appel réseau au démarrage (local_status seul). Intégration : tab_usage exécute via le moteur réel selon ConfigState (make_process_fn), anti double-lancement UI. app.py câble Config↔Usage↔licence. G3-D build-prep : anonymisation_gui_v6_onefile.spec (entry V6, customtkinter + modules gui_v6 en hiddenimports). Installateur Anonymisation.iss produit déjà la cible Anonymisation-Setup.exe. Aucun artefact .exe commité ; build Windows à part. Tests +14 (engine_bridge 8, config_state 6). self-test exit 0, 46 tests gui_v6, 193 tests/unit (0 régression). Moteur/V5/specs CLI intacts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
tests/unit/test_gui_v6_config_state.py
Normal file
56
tests/unit/test_gui_v6_config_state.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""Tests de l'état de configuration G3-B (profils/options résolus sans fichiers réels)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from gui_v6.config_state import ConfigState, default_profile_key, list_profile_keys
|
||||
from gui_v6.engine_bridge import EngineSettings
|
||||
|
||||
|
||||
def test_to_engine_settings_defaults():
|
||||
settings = ConfigState().to_engine_settings(config_path=Path("/tmp/c.yml"))
|
||||
assert isinstance(settings, EngineSettings)
|
||||
assert settings.make_vector_redaction is False
|
||||
assert settings.also_make_raster_burn is True
|
||||
assert settings.use_local_ner is True
|
||||
assert settings.config_path == Path("/tmp/c.yml")
|
||||
|
||||
|
||||
def test_to_engine_settings_custom():
|
||||
state = ConfigState(
|
||||
profile="oncologie",
|
||||
raster_burn=False,
|
||||
use_local_ner=False,
|
||||
enable_gliner=True,
|
||||
ogc_label="OCG",
|
||||
)
|
||||
settings = state.to_engine_settings()
|
||||
assert settings.also_make_raster_burn is False
|
||||
assert settings.use_local_ner is False
|
||||
assert settings.enable_gliner is True
|
||||
assert settings.profile == "oncologie"
|
||||
assert settings.ogc_label == "OCG"
|
||||
|
||||
|
||||
def test_list_profile_keys_injected():
|
||||
keys = list_profile_keys(lister=lambda: {"b": {}, "a": {}})
|
||||
assert keys == ["a", "b"]
|
||||
|
||||
|
||||
def test_list_profile_keys_failure_returns_empty():
|
||||
def boom():
|
||||
raise RuntimeError("pas de profils")
|
||||
|
||||
assert list_profile_keys(lister=boom) == []
|
||||
|
||||
|
||||
def test_default_profile_key_injected():
|
||||
assert default_profile_key(getter=lambda: "defaut") == "defaut"
|
||||
|
||||
|
||||
def test_default_profile_key_failure_returns_none():
|
||||
def boom():
|
||||
raise RuntimeError("ko")
|
||||
|
||||
assert default_profile_key(getter=boom) is None
|
||||
156
tests/unit/test_gui_v6_engine_bridge.py
Normal file
156
tests/unit/test_gui_v6_engine_bridge.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""Tests du pont moteur G3-A : kwargs corrects, managers lazy, engine injecté.
|
||||
|
||||
Aucun vrai manager, aucun modèle, aucun réseau : tout est injecté via factories.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from gui_v6.engine_bridge import (
|
||||
EngineSettings,
|
||||
ManagerState,
|
||||
NerManagers,
|
||||
build_engine_kwargs,
|
||||
make_process_fn,
|
||||
)
|
||||
|
||||
|
||||
class FakeManager:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
|
||||
def _counting_factories(counter, fail_camembert=False):
|
||||
def camembert():
|
||||
counter["camembert"] += 1
|
||||
if fail_camembert:
|
||||
raise RuntimeError("modèle absent")
|
||||
return FakeManager("camembert")
|
||||
|
||||
def eds():
|
||||
counter["eds"] += 1
|
||||
return FakeManager("eds")
|
||||
|
||||
def gliner():
|
||||
counter["gliner"] += 1
|
||||
return FakeManager("gliner")
|
||||
|
||||
return {"camembert": camembert, "eds": eds, "gliner": gliner}
|
||||
|
||||
|
||||
# -- kwargs ----------------------------------------------------------------
|
||||
|
||||
def test_kwargs_defaults_v5_like():
|
||||
settings = EngineSettings(config_path=Path("/tmp/cfg.yml"), ogc_label="OCG")
|
||||
kwargs = build_engine_kwargs(settings, managers=None)
|
||||
assert kwargs["make_vector_redaction"] is False
|
||||
assert kwargs["also_make_raster_burn"] is True
|
||||
assert kwargs["config_path"] == Path("/tmp/cfg.yml")
|
||||
assert kwargs["ogc_label"] == "OCG"
|
||||
# Sans managers : pas de NER.
|
||||
assert kwargs["use_hf"] is False
|
||||
|
||||
|
||||
def test_kwargs_with_loaded_managers():
|
||||
settings = EngineSettings(enable_eds=True, enable_gliner=True)
|
||||
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
||||
managers = NerManagers(settings, factories=_counting_factories(counter))
|
||||
managers.ensure_loaded()
|
||||
kwargs = build_engine_kwargs(settings, managers)
|
||||
assert kwargs["use_hf"] is True
|
||||
assert kwargs["camembert_manager"].name == "camembert"
|
||||
assert kwargs["ner_manager"].name == "eds"
|
||||
assert kwargs["gliner_manager"].name == "gliner"
|
||||
|
||||
|
||||
def test_kwargs_ner_disabled():
|
||||
settings = EngineSettings(use_local_ner=False)
|
||||
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
||||
managers = NerManagers(settings, factories=_counting_factories(counter))
|
||||
managers.ensure_loaded()
|
||||
kwargs = build_engine_kwargs(settings, managers)
|
||||
assert kwargs["use_hf"] is False
|
||||
assert counter["camembert"] == 0 # NER désactivé : rien chargé
|
||||
|
||||
|
||||
# -- lazy loading ----------------------------------------------------------
|
||||
|
||||
def test_managers_not_loaded_on_init():
|
||||
settings = EngineSettings()
|
||||
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
||||
NerManagers(settings, factories=_counting_factories(counter))
|
||||
# Aucune factory appelée à la construction.
|
||||
assert counter == {"camembert": 0, "eds": 0, "gliner": 0}
|
||||
|
||||
|
||||
def test_managers_load_once_and_state():
|
||||
settings = EngineSettings(enable_eds=True)
|
||||
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
||||
managers = NerManagers(settings, factories=_counting_factories(counter))
|
||||
assert managers.state == ManagerState.NOT_LOADED
|
||||
assert managers.ensure_loaded() == ManagerState.READY
|
||||
assert managers.ensure_loaded() == ManagerState.READY # idempotent
|
||||
assert counter["camembert"] == 1 # chargé une seule fois
|
||||
assert counter["eds"] == 1
|
||||
assert counter["gliner"] == 0 # non activé
|
||||
|
||||
|
||||
def test_managers_unavailable_when_camembert_fails():
|
||||
settings = EngineSettings()
|
||||
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
||||
managers = NerManagers(settings, factories=_counting_factories(counter, fail_camembert=True))
|
||||
assert managers.ensure_loaded() == ManagerState.UNAVAILABLE
|
||||
assert managers.use_hf is False
|
||||
|
||||
|
||||
def test_optional_manager_failure_is_tolerated():
|
||||
settings = EngineSettings(enable_gliner=True)
|
||||
|
||||
def factories():
|
||||
def camembert():
|
||||
return FakeManager("camembert")
|
||||
|
||||
def gliner():
|
||||
raise RuntimeError("gliner ko")
|
||||
|
||||
def eds():
|
||||
return FakeManager("eds")
|
||||
|
||||
return {"camembert": camembert, "eds": eds, "gliner": gliner}
|
||||
|
||||
managers = NerManagers(settings, factories=factories())
|
||||
assert managers.ensure_loaded() == ManagerState.READY # gliner ko ne bloque pas
|
||||
assert managers.use_hf is True
|
||||
|
||||
|
||||
# -- make_process_fn -------------------------------------------------------
|
||||
|
||||
def test_process_fn_calls_engine_with_kwargs(tmp_path):
|
||||
settings = EngineSettings()
|
||||
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
||||
managers = NerManagers(settings, factories=_counting_factories(counter))
|
||||
captured = {}
|
||||
|
||||
def fake_engine(doc_path, out_dir, **kwargs):
|
||||
captured["doc"] = doc_path
|
||||
captured["out"] = out_dir
|
||||
captured["kwargs"] = kwargs
|
||||
return {"status": "ok"}
|
||||
|
||||
fn = make_process_fn(settings, managers=managers, engine=fake_engine)
|
||||
# Avant tout traitement, aucun manager chargé.
|
||||
assert counter["camembert"] == 0
|
||||
|
||||
result = fn(tmp_path / "doc.pdf", tmp_path / "out")
|
||||
|
||||
assert result == {"status": "ok"}
|
||||
assert captured["doc"] == tmp_path / "doc.pdf"
|
||||
assert captured["kwargs"]["make_vector_redaction"] is False
|
||||
assert captured["kwargs"]["also_make_raster_burn"] is True
|
||||
assert captured["kwargs"]["use_hf"] is True
|
||||
assert captured["kwargs"]["camembert_manager"].name == "camembert"
|
||||
# Le chargement n'a eu lieu qu'à l'appel de process_fn.
|
||||
assert counter["camembert"] == 1
|
||||
Reference in New Issue
Block a user