Files
RAG/tests/test_rag.py
2025-11-03 18:20:12 +01:00

423 lines
15 KiB
Python

"""
Suite de tests unitaires pour le système RAG (Indexer, Datastore, Retriever)
Adapté à votre implémentation spécifique avec:
- BaseDatastore (pas BaseDataStore)
- Datastore (pas DataStore)
- DataItem avec content/source (pas text/metadata)
Pour exécuter:
pytest tests/test_rag.py -v
Pour exécuter avec couverture:
pytest tests/test_rag.py --cov=src --cov-report=html
"""
import sys
from pathlib import Path
# Configuration du PYTHONPATH
project_root = Path(__file__).parent.parent
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
import pytest
import tempfile
import shutil
from typing import List
# Imports adaptés à votre structure
from src.interface.base_datastore import BaseDatastore, DataItem
from src.impl.datastore import Datastore
# Tentative d'import des autres composants (adapter selon vos fichiers)
try:
from src.interface.base_indexer import BaseIndexer
from src.impl.indexer import Indexer
HAS_INDEXER = True
except ImportError:
HAS_INDEXER = False
print("⚠️ Indexer non trouvé - tests d'indexation désactivés")
try:
from src.interface.base_retriever import BaseRetriever
from src.impl.retriever import Retriever
HAS_RETRIEVER = True
except ImportError:
HAS_RETRIEVER = False
print("⚠️ Retriever non trouvé - tests de récupération désactivés")
# ============================================================================
# FIXTURES - Configuration des tests
# ============================================================================
@pytest.fixture
def temp_dir():
"""Crée un répertoire temporaire pour les tests."""
temp_path = tempfile.mkdtemp()
yield temp_path
shutil.rmtree(temp_path, ignore_errors=True)
@pytest.fixture
def sample_items():
"""Crée des DataItems de test."""
return [
DataItem(
content="L'intelligence artificielle (IA) est un domaine de l'informatique.",
source="doc1.pdf"
),
DataItem(
content="Python est un langage de programmation populaire pour l'IA.",
source="doc2.pdf"
),
DataItem(
content="Les réseaux de neurones sont utilisés en deep learning.",
source="doc3.pdf"
),
DataItem(
content="Le machine learning permet aux ordinateurs d'apprendre sans être explicitement programmés.",
source="doc4.pdf"
),
]
@pytest.fixture
def datastore(temp_dir, monkeypatch):
"""Crée une instance du Datastore avec une base temporaire."""
# Modifier le chemin de la DB pour les tests
test_db_path = str(Path(temp_dir) / "test-lancedb")
monkeypatch.setattr("src.impl.datastore.Datastore.DB_PATH", test_db_path)
ds = Datastore()
ds.reset_table() # S'assurer que la table est vide
yield ds
# Cleanup
try:
ds.vector_db.drop_table(ds.DB_TABLE_NAME)
except:
pass
@pytest.fixture
def populated_datastore(datastore, sample_items):
"""Datastore pré-rempli avec des items."""
datastore.add_items(sample_items)
return datastore
@pytest.fixture
def indexer():
"""Crée une instance de l'Indexer si disponible."""
if not HAS_INDEXER:
pytest.skip("Indexer non disponible")
return Indexer()
@pytest.fixture
def retriever(datastore):
"""Crée une instance du Retriever si disponible."""
if not HAS_RETRIEVER:
pytest.skip("Retriever non disponible")
return Retriever(datastore=datastore)
# ============================================================================
# TESTS DATAITEM
# ============================================================================
class TestDataItem:
"""Tests pour la classe DataItem."""
def test_dataitem_creation(self):
"""Test la création d'un DataItem."""
item = DataItem(content="Test content", source="test.pdf")
assert item.content == "Test content"
assert item.source == "test.pdf"
def test_dataitem_default_values(self):
"""Test les valeurs par défaut."""
item = DataItem()
assert item.content == ""
assert item.source == ""
def test_dataitem_validation(self):
"""Test la validation Pydantic."""
# Pydantic devrait accepter ces types
item = DataItem(content="text", source="source.pdf")
assert isinstance(item.content, str)
assert isinstance(item.source, str)
# ============================================================================
# TESTS DATASTORE
# ============================================================================
class TestDatastore:
"""Tests pour la classe Datastore."""
def test_datastore_initialization(self, datastore):
"""Test l'initialisation du Datastore."""
assert isinstance(datastore, BaseDatastore)
assert datastore.vector_dimensions == 384
assert datastore.model is not None
assert datastore.table is not None
def test_reset_table(self, datastore):
"""Test le reset de la table."""
# Ajouter des items
items = [DataItem(content="test", source="test.pdf")]
datastore.add_items(items)
# Reset
table = datastore.reset_table()
assert table is not None
# Vérifier que la table est vide
results = datastore.search_datastore("test", top_k=10)
assert len(results) == 0
def test_add_single_item(self, datastore):
"""Test l'ajout d'un seul item."""
item = DataItem(
content="Test de l'ajout d'un item unique",
source="test_single.pdf"
)
datastore.add_items([item])
# Vérifier que l'item peut être retrouvé
results = datastore.search_datastore("test ajout", top_k=5)
assert len(results) > 0
def test_add_multiple_items(self, datastore, sample_items):
"""Test l'ajout de plusieurs items."""
datastore.add_items(sample_items)
# Vérifier que plusieurs items ont été ajoutés
results = datastore.search_datastore("intelligence", top_k=10)
assert len(results) > 0
def test_add_empty_list(self, datastore):
"""Test l'ajout d'une liste vide."""
# Ne devrait pas crasher
datastore.add_items([])
# Pas d'exception = succès
def test_create_vector(self, datastore):
"""Test la création de vecteurs."""
vector = datastore.create_vector("Test content")
assert isinstance(vector, list)
assert len(vector) == 384 # Dimension du modèle all-MiniLM-L6-v2
assert all(isinstance(v, float) for v in vector)
def test_create_vector_consistency(self, datastore):
"""Test que le même texte produit le même vecteur."""
text = "Texte de test pour la cohérence"
vector1 = datastore.create_vector(text)
vector2 = datastore.create_vector(text)
# Les vecteurs devraient être identiques (ou très proches)
import numpy as np
similarity = np.dot(vector1, vector2) / (np.linalg.norm(vector1) * np.linalg.norm(vector2))
assert similarity > 0.99 # Très haute similarité
def test_search_basic(self, populated_datastore):
"""Test une recherche basique."""
results = populated_datastore.search_datastore("intelligence artificielle", top_k=3)
assert len(results) > 0
assert len(results) <= 3
assert all(isinstance(r, str) for r in results)
def test_search_relevance(self, populated_datastore):
"""Test la pertinence des résultats."""
results = populated_datastore.search_datastore("Python programmation", top_k=5)
assert len(results) > 0
# Le premier résultat devrait contenir "Python"
assert any("Python" in results[0] or "python" in results[0].lower()
for _ in [results[0]])
def test_search_top_k_limit(self, populated_datastore):
"""Test que top_k limite correctement les résultats."""
results_2 = populated_datastore.search_datastore("test", top_k=2)
results_4 = populated_datastore.search_datastore("test", top_k=4)
assert len(results_2) <= 2
assert len(results_4) <= 4
def test_search_empty_query(self, populated_datastore):
"""Test avec une requête vide."""
results = populated_datastore.search_datastore("", top_k=5)
# Devrait retourner des résultats ou liste vide, pas d'erreur
assert isinstance(results, list)
def test_search_no_results(self, datastore):
"""Test une recherche sur une base vide."""
results = datastore.search_datastore("query inexistante", top_k=5)
assert results == []
def test_search_special_characters(self, populated_datastore):
"""Test avec des caractères spéciaux."""
results = populated_datastore.search_datastore("l'intelligence", top_k=5)
assert isinstance(results, list)
def test_multiple_searches_consistency(self, populated_datastore):
"""Test la cohérence sur plusieurs recherches."""
query = "intelligence artificielle"
results1 = populated_datastore.search_datastore(query, top_k=3)
results2 = populated_datastore.search_datastore(query, top_k=3)
# Les résultats devraient être identiques
assert results1 == results2
def test_large_batch_add(self, datastore):
"""Test l'ajout d'un grand nombre d'items."""
large_batch = [
DataItem(content=f"Document numéro {i} avec du contenu varié.", source=f"doc_{i}.pdf")
for i in range(100)
]
# Ne devrait pas crasher
datastore.add_items(large_batch)
# Vérifier que des résultats sont retournés
results = datastore.search_datastore("document", top_k=10)
assert len(results) > 0
# ============================================================================
# TESTS INDEXER (si disponible)
# ============================================================================
@pytest.mark.skipif(not HAS_INDEXER, reason="Indexer non disponible")
class TestIndexer:
"""Tests pour la classe Indexer."""
def test_indexer_initialization(self, indexer):
"""Test l'initialisation de l'Indexer."""
assert isinstance(indexer, BaseIndexer)
def test_index_documents(self, indexer, temp_dir):
"""Test l'indexation de documents."""
# Créer un document de test
doc_path = Path(temp_dir) / "test_doc.pdf"
doc_path.write_text("Contenu de test pour l'indexation.")
items = indexer.index([str(doc_path)])
assert len(items) > 0
assert all(isinstance(item, DataItem) for item in items)
assert all(hasattr(item, 'content') for item in items)
assert all(hasattr(item, 'source') for item in items)
# ============================================================================
# TESTS RETRIEVER (si disponible)
# ============================================================================
@pytest.mark.skipif(not HAS_RETRIEVER, reason="Retriever non disponible")
class TestRetriever:
"""Tests pour la classe Retriever."""
def test_retriever_initialization(self, retriever):
"""Test l'initialisation du Retriever."""
assert isinstance(retriever, BaseRetriever)
assert retriever.datastore is not None
def test_retrieve_basic(self, retriever, populated_datastore):
"""Test une récupération basique."""
retriever.datastore = populated_datastore
results = retriever.search_retriever("intelligence artificielle", top_k=3)
assert len(results) > 0
assert len(results) <= 3
# ============================================================================
# TESTS D'INTÉGRATION
# ============================================================================
class TestIntegration:
"""Tests d'intégration du système complet."""
@pytest.mark.skipif(not HAS_INDEXER, reason="Indexer requis")
def test_full_pipeline(self, indexer, datastore, temp_dir):
"""Test du pipeline complet: indexation → stockage → recherche."""
# 1. Créer un document
doc_path = Path(temp_dir) / "integration_test.pdf"
doc_path.write_text("""
L'intelligence artificielle transforme le monde.
Python est le langage privilégié pour l'IA.
Les algorithmes de machine learning sont puissants.
""")
# 2. Indexation
items = indexer.index([str(doc_path)])
assert len(items) > 0
# 3. Stockage
datastore.add_items(items)
# 4. Recherche
results = datastore.search_datastore("intelligence artificielle Python", top_k=3)
assert len(results) > 0
# 5. Vérifier la pertinence
top_result = results[0].lower()
assert "intelligence" in top_result or "python" in top_result
def test_incremental_addition(self, datastore, sample_items):
"""Test l'ajout incrémental de données."""
# Ajouter en plusieurs fois
datastore.add_items(sample_items[:2])
results_1 = datastore.search_datastore("test", top_k=10)
datastore.add_items(sample_items[2:])
results_2 = datastore.search_datastore("test", top_k=10)
# Le second devrait avoir plus de résultats potentiels
assert len(results_2) >= len(results_1)
# ============================================================================
# TESTS DE ROBUSTESSE
# ============================================================================
class TestRobustness:
"""Tests de robustesse et gestion d'erreurs."""
def test_unicode_content(self, datastore):
"""Test avec du contenu Unicode."""
items = [
DataItem(content="Émojis: 🎉 🎨 🚀", source="unicode.pdf"),
DataItem(content="Caractères spéciaux: é à ç ñ", source="special.pdf"),
]
datastore.add_items(items)
results = datastore.search_datastore("émojis caractères", top_k=5)
assert isinstance(results, list)
def test_very_long_content(self, datastore):
"""Test avec du contenu très long."""
long_content = "Test. " * 1000 # ~6000 caractères
item = DataItem(content=long_content, source="long.pdf")
# Ne devrait pas crasher
datastore.add_items([item])
results = datastore.search_datastore("test", top_k=1)
assert len(results) >= 0
def test_empty_content(self, datastore):
"""Test avec du contenu vide."""
items = [DataItem(content="", source="empty.pdf")]
# Ne devrait pas crasher
datastore.add_items(items)
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])