""" Configuration pytest et Hypothesis pour les tests du Pipeline MCO PMSI. Ce fichier configure : - Les profils Hypothesis pour différents environnements - Les fixtures communes à tous les tests - Les hooks pytest personnalisés """ import os from pathlib import Path from typing import Generator import pytest from hypothesis import HealthCheck, Phase, Verbosity, settings # ============================================================================ # Configuration Hypothesis # ============================================================================ # Profil par défaut : équilibré entre vitesse et couverture settings.register_profile( "default", max_examples=100, deadline=5000, # 5 secondes par test case derandomize=False, print_blob=True, verbosity=Verbosity.normal, phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target, Phase.shrink], suppress_health_check=[HealthCheck.too_slow], ) # Profil CI : plus d'exemples pour meilleure couverture settings.register_profile( "ci", max_examples=200, deadline=10000, # 10 secondes par test case derandomize=True, # Reproductible en CI print_blob=True, verbosity=Verbosity.verbose, phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target, Phase.shrink], ) # Profil dev : rapide pour développement settings.register_profile( "dev", max_examples=20, deadline=2000, # 2 secondes par test case derandomize=False, print_blob=False, verbosity=Verbosity.normal, phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.shrink], suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], ) # Profil debug : minimal pour debugging settings.register_profile( "debug", max_examples=10, deadline=None, # Pas de deadline pour debugging derandomize=True, print_blob=True, verbosity=Verbosity.debug, phases=[Phase.explicit, Phase.reuse, Phase.generate], ) # Profil exhaustif : pour validation finale settings.register_profile( "exhaustive", max_examples=1000, deadline=30000, # 30 secondes par test case derandomize=True, print_blob=True, verbosity=Verbosity.verbose, phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target, Phase.shrink], ) # Charger le profil depuis la variable d'environnement ou utiliser 'default' profile = os.getenv("HYPOTHESIS_PROFILE", "default") settings.load_profile(profile) # ============================================================================ # Fixtures communes # ============================================================================ @pytest.fixture(scope="session") def project_root() -> Path: """Retourne le chemin racine du projet.""" return Path(__file__).parent.parent @pytest.fixture(scope="session") def data_dir(project_root: Path) -> Path: """Retourne le répertoire data/.""" return project_root / "data" @pytest.fixture(scope="session") def referentiels_dir(data_dir: Path) -> Path: """Retourne le répertoire des référentiels.""" return data_dir / "referentiels" @pytest.fixture(scope="session") def config_dir(project_root: Path) -> Path: """Retourne le répertoire config/.""" return project_root / "config" @pytest.fixture(scope="session") def logs_dir(project_root: Path) -> Path: """Retourne le répertoire logs/.""" logs = project_root / "logs" logs.mkdir(exist_ok=True) return logs @pytest.fixture def temp_db_path(tmp_path: Path) -> Path: """Crée un chemin pour une base de données temporaire.""" return tmp_path / "test_db.sqlite" # ============================================================================ # Hooks pytest # ============================================================================ def pytest_configure(config: pytest.Config) -> None: """Configuration pytest au démarrage.""" # Créer le répertoire logs s'il n'existe pas logs_dir = Path("logs") logs_dir.mkdir(exist_ok=True) # Afficher le profil Hypothesis utilisé print(f"\n🔬 Hypothesis profile: {profile}") print(f" Max examples: {settings.default.max_examples}") print(f" Deadline: {settings.default.deadline}ms\n") def pytest_collection_modifyitems(config: pytest.Config, items: list) -> None: """Modifier les items de test collectés.""" # Ajouter automatiquement le marker 'property' aux tests utilisant Hypothesis for item in items: if "hypothesis" in item.keywords: item.add_marker(pytest.mark.property) item.add_marker(pytest.mark.pbt) def pytest_addoption(parser: pytest.Parser) -> None: """Ajouter des options de ligne de commande personnalisées.""" parser.addoption( "--run-slow", action="store_true", default=False, help="Exécuter les tests marqués comme 'slow'", ) parser.addoption( "--run-gpu", action="store_true", default=False, help="Exécuter les tests nécessitant un GPU", ) def pytest_runtest_setup(item: pytest.Item) -> None: """Hook exécuté avant chaque test.""" # Skip les tests 'slow' sauf si --run-slow est spécifié if "slow" in item.keywords and not item.config.getoption("--run-slow"): pytest.skip("need --run-slow option to run") # Skip les tests 'gpu' sauf si --run-gpu est spécifié if "gpu" in item.keywords and not item.config.getoption("--run-gpu"): pytest.skip("need --run-gpu option to run") # ============================================================================ # Fixtures pour les tests de propriétés # ============================================================================ @pytest.fixture def hypothesis_seed() -> int: """Seed fixe pour reproductibilité des tests de propriétés.""" return 42 # ============================================================================ # Utilitaires de test # ============================================================================ @pytest.fixture def sample_clinical_text() -> str: """Texte clinique d'exemple pour les tests.""" return """ COMPTE RENDU MÉDICAL Patient admis pour douleurs abdominales aiguës. ANAMNÈSE: Douleurs abdominales depuis 48h, localisées au niveau de la fosse iliaque droite. Pas de fièvre. Pas de vomissements. EXAMEN CLINIQUE: Défense abdominale à la palpation de la FID. Signe de Blumberg positif. DIAGNOSTIC: Appendicite aiguë. TRAITEMENT: Appendicectomie réalisée le 15/01/2024. Suites opératoires simples. """ @pytest.fixture def sample_clinical_text_with_negation() -> str: """Texte clinique avec négations pour tester les qualificateurs.""" return """ COMPTE RENDU MÉDICAL Patient admis pour bilan. ANAMNÈSE: Pas de douleurs thoraciques. Absence de dyspnée. Pas d'antécédent de diabète. EXAMEN CLINIQUE: Auscultation cardiaque normale. Pas de souffle cardiaque. DIAGNOSTIC: Bilan de santé normal. """ @pytest.fixture def sample_clinical_text_with_suspicion() -> str: """Texte clinique avec suspicions pour tester les qualificateurs.""" return """ COMPTE RENDU MÉDICAL Patient admis pour exploration. ANAMNÈSE: Possible infection urinaire. Suspicion de pyélonéphrite. EXAMENS: ECBU en attente. DIAGNOSTIC: À confirmer après résultats ECBU. """ # ============================================================================ # Fixtures pour le pipeline # ============================================================================ @pytest.fixture def db_session(temp_db_path: Path) -> Generator: """Crée une session de base de données temporaire pour les tests.""" from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from pipeline_mco_pmsi.database.base import Base # Créer le moteur de base de données engine = create_engine(f"sqlite:///{temp_db_path}") # Créer toutes les tables Base.metadata.create_all(engine) # Créer une session Session = sessionmaker(bind=engine) session = Session() try: yield session finally: session.close() engine.dispose() @pytest.fixture def rag_engine(tmp_path): """Crée un moteur RAG mock pour les tests.""" from pipeline_mco_pmsi.rag.rag_engine import RAGEngine from pipeline_mco_pmsi.rag.referentiels_manager import ReferentielsManager # Créer un répertoire temporaire pour les référentiels ref_dir = tmp_path / "referentiels" ref_dir.mkdir(exist_ok=True) # Créer un ReferentielsManager mock referentiels_manager = ReferentielsManager( data_dir=ref_dir, embedding_model="mock", ) # Créer le RAG Engine rag_engine = RAGEngine( referentiels_manager=referentiels_manager, data_dir=ref_dir, ) return rag_engine @pytest.fixture def sample_stay(db_session): """Crée un séjour d'exemple pour les tests.""" from datetime import datetime, timedelta from pipeline_mco_pmsi.models.metadata import StayMetadata from pipeline_mco_pmsi.database.models import StayDB stay_metadata = StayMetadata( stay_id="test_stay_001", admission_date=datetime.now() - timedelta(days=3), discharge_date=datetime.now(), specialty="gastro-enterologie", unit="medecine", age=45, sex="M", ) # Créer le Stay dans la base de données stay_db = StayDB( stay_id=stay_metadata.stay_id, admission_date=stay_metadata.admission_date, discharge_date=stay_metadata.discharge_date, specialty=stay_metadata.specialty, unit=stay_metadata.unit, age=stay_metadata.age, sex=stay_metadata.sex, ) db_session.add(stay_db) db_session.commit() return stay_metadata