- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
592 lines
20 KiB
Python
592 lines
20 KiB
Python
"""
|
|
Tests unitaires pour EffectiveLRUCache
|
|
|
|
Tests pour l'exigence 6.1: Implémenter EffectiveLRUCache
|
|
- Limites de taille ET de mémoire effectives
|
|
- Éviction basée sur l'utilisation mémoire réelle
|
|
"""
|
|
|
|
import pytest
|
|
import time
|
|
import numpy as np
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from core.execution.memory_cache import (
|
|
EffectiveLRUCache,
|
|
MemoryManager,
|
|
MemoryEstimator,
|
|
get_memory_manager,
|
|
shutdown_memory_manager
|
|
)
|
|
|
|
|
|
class TestMemoryEstimator:
|
|
"""Tests pour l'estimateur de mémoire."""
|
|
|
|
def test_estimate_none(self):
|
|
"""Test estimation pour None."""
|
|
assert MemoryEstimator.estimate_size(None) == 0
|
|
|
|
def test_estimate_numpy_array(self):
|
|
"""Test estimation pour numpy array."""
|
|
arr = np.zeros((100, 100), dtype=np.float32)
|
|
expected_size = 100 * 100 * 4 # 4 bytes per float32
|
|
assert MemoryEstimator.estimate_size(arr) == expected_size
|
|
|
|
def test_estimate_string(self):
|
|
"""Test estimation pour string."""
|
|
text = "Hello World"
|
|
size = MemoryEstimator.estimate_size(text)
|
|
assert size > 0
|
|
assert isinstance(size, int)
|
|
|
|
def test_estimate_list(self):
|
|
"""Test estimation pour liste."""
|
|
data = [1, 2, 3, "test", [4, 5]]
|
|
size = MemoryEstimator.estimate_size(data)
|
|
assert size > 0
|
|
assert isinstance(size, int)
|
|
|
|
def test_estimate_dict(self):
|
|
"""Test estimation pour dictionnaire."""
|
|
data = {"key1": "value1", "key2": [1, 2, 3]}
|
|
size = MemoryEstimator.estimate_size(data)
|
|
assert size > 0
|
|
assert isinstance(size, int)
|
|
|
|
|
|
class TestEffectiveLRUCache:
|
|
"""Tests pour EffectiveLRUCache."""
|
|
|
|
def setup_method(self):
|
|
"""Setup pour chaque test."""
|
|
self.cache = EffectiveLRUCache(
|
|
max_size=5,
|
|
max_memory_mb=1.0, # 1MB
|
|
enable_monitoring=False # Désactiver pour les tests
|
|
)
|
|
|
|
def teardown_method(self):
|
|
"""Cleanup après chaque test."""
|
|
if hasattr(self, 'cache'):
|
|
try:
|
|
self.cache.stop_monitoring()
|
|
except Exception:
|
|
pass # Ignorer les erreurs de cleanup
|
|
|
|
def test_basic_operations(self):
|
|
"""Test opérations de base."""
|
|
# Put et get
|
|
assert self.cache.put("key1", "value1")
|
|
assert self.cache.get("key1") == "value1"
|
|
|
|
# Miss
|
|
assert self.cache.get("nonexistent") is None
|
|
|
|
# Contains
|
|
assert "key1" in self.cache
|
|
assert "nonexistent" not in self.cache
|
|
|
|
# Length
|
|
assert len(self.cache) == 1
|
|
|
|
def test_lru_eviction_by_size(self):
|
|
"""Test éviction LRU par taille."""
|
|
# Remplir le cache
|
|
for i in range(5):
|
|
assert self.cache.put(f"key{i}", f"value{i}")
|
|
|
|
assert len(self.cache) == 5
|
|
|
|
# Ajouter un 6ème élément doit évict le premier
|
|
assert self.cache.put("key5", "value5")
|
|
assert len(self.cache) == 5
|
|
assert self.cache.get("key0") is None # Évicté
|
|
assert self.cache.get("key1") == "value1" # Toujours là
|
|
|
|
def test_lru_eviction_by_memory(self):
|
|
"""Test éviction LRU par mémoire."""
|
|
# Créer plusieurs objets moyens qui ensemble dépassent la limite
|
|
medium_arrays = []
|
|
for i in range(4):
|
|
arr = np.zeros((128, 128), dtype=np.float32) # ~64KB chacun
|
|
medium_arrays.append(arr)
|
|
assert self.cache.put(f"medium{i}", arr)
|
|
|
|
# À ce point, on a ~256KB dans le cache
|
|
assert len(self.cache) == 4
|
|
|
|
# Ajouter un gros objet qui doit évict plusieurs petits
|
|
big_array = np.zeros((512, 512), dtype=np.float32) # ~1MB
|
|
assert self.cache.put("big", big_array)
|
|
|
|
# Le gros objet doit avoir évicté les petits pour faire de la place
|
|
assert self.cache.get("big") is not None
|
|
|
|
# Vérifier qu'au moins quelques objets ont été évictés
|
|
remaining_mediums = sum(1 for i in range(4) if self.cache.get(f"medium{i}") is not None)
|
|
assert remaining_mediums < 4 # Au moins un a été évicté
|
|
|
|
def test_memory_limit_rejection(self):
|
|
"""Test rejet d'objets trop gros."""
|
|
# Créer un objet plus gros que la limite du cache
|
|
huge_array = np.zeros((1024, 1024), dtype=np.float32) # ~4MB > 1MB limit
|
|
|
|
# Doit être rejeté
|
|
assert not self.cache.put("huge", huge_array)
|
|
assert self.cache.get("huge") is None
|
|
assert len(self.cache) == 0
|
|
|
|
def test_update_existing_key(self):
|
|
"""Test mise à jour d'une clé existante."""
|
|
# Ajouter une valeur
|
|
assert self.cache.put("key1", "value1")
|
|
assert self.cache.get("key1") == "value1"
|
|
|
|
# Mettre à jour
|
|
assert self.cache.put("key1", "new_value")
|
|
assert self.cache.get("key1") == "new_value"
|
|
assert len(self.cache) == 1
|
|
|
|
def test_lru_order(self):
|
|
"""Test ordre LRU."""
|
|
# Ajouter des éléments
|
|
for i in range(3):
|
|
self.cache.put(f"key{i}", f"value{i}")
|
|
|
|
# Accéder à key0 pour le rendre récent
|
|
self.cache.get("key0")
|
|
|
|
# Ajouter plus d'éléments pour déclencher éviction
|
|
for i in range(3, 6):
|
|
self.cache.put(f"key{i}", f"value{i}")
|
|
|
|
# key0 doit toujours être là (récemment accédé)
|
|
assert self.cache.get("key0") == "value0"
|
|
# key1 doit avoir été évicté (plus ancien)
|
|
assert self.cache.get("key1") is None
|
|
|
|
def test_remove(self):
|
|
"""Test suppression d'éléments."""
|
|
# Ajouter des éléments
|
|
self.cache.put("key1", "value1")
|
|
self.cache.put("key2", "value2")
|
|
|
|
# Supprimer
|
|
assert self.cache.remove("key1")
|
|
assert self.cache.get("key1") is None
|
|
assert self.cache.get("key2") == "value2"
|
|
assert len(self.cache) == 1
|
|
|
|
# Supprimer inexistant
|
|
assert not self.cache.remove("nonexistent")
|
|
|
|
def test_clear(self):
|
|
"""Test vidage du cache."""
|
|
# Ajouter des éléments
|
|
for i in range(3):
|
|
self.cache.put(f"key{i}", f"value{i}")
|
|
|
|
assert len(self.cache) == 3
|
|
|
|
# Vider
|
|
self.cache.clear()
|
|
assert len(self.cache) == 0
|
|
|
|
for i in range(3):
|
|
assert self.cache.get(f"key{i}") is None
|
|
|
|
def test_cleanup_old_entries(self):
|
|
"""Test nettoyage des entrées anciennes."""
|
|
# Ajouter des éléments
|
|
self.cache.put("old1", "value1")
|
|
self.cache.put("old2", "value2")
|
|
|
|
# Simuler le passage du temps
|
|
old_time = datetime.now() - timedelta(hours=2)
|
|
self.cache._access_times["old1"] = old_time
|
|
self.cache._access_times["old2"] = old_time
|
|
|
|
# Ajouter un élément récent
|
|
self.cache.put("recent", "value_recent")
|
|
|
|
# Nettoyer les entrées de plus d'1 heure
|
|
cleaned = self.cache.cleanup_old_entries(max_age_hours=1.0)
|
|
|
|
assert cleaned == 2
|
|
assert self.cache.get("old1") is None
|
|
assert self.cache.get("old2") is None
|
|
assert self.cache.get("recent") == "value_recent"
|
|
|
|
def test_memory_usage_stats(self):
|
|
"""Test statistiques d'utilisation mémoire."""
|
|
# Ajouter quelques éléments
|
|
self.cache.put("key1", "value1")
|
|
self.cache.put("key2", np.zeros(100, dtype=np.float32))
|
|
|
|
usage = self.cache.get_memory_usage()
|
|
|
|
assert usage['current_bytes'] > 0
|
|
assert usage['current_mb'] > 0
|
|
assert usage['max_bytes'] == 1024 * 1024 # 1MB
|
|
assert usage['max_mb'] == 1.0
|
|
assert 0 <= usage['usage_percent'] <= 100
|
|
assert usage['items_count'] == 2
|
|
assert usage['max_items'] == 5
|
|
assert usage['avg_item_size'] > 0
|
|
|
|
def test_comprehensive_stats(self):
|
|
"""Test statistiques complètes."""
|
|
# Générer quelques hits et misses
|
|
self.cache.put("key1", "value1")
|
|
self.cache.get("key1") # hit
|
|
self.cache.get("nonexistent") # miss
|
|
|
|
stats = self.cache.get_stats()
|
|
|
|
assert stats['hits'] == 1
|
|
assert stats['misses'] == 1
|
|
assert stats['total_requests'] == 2
|
|
assert stats['hit_rate'] == 0.5
|
|
assert stats['evictions'] == 0
|
|
assert stats['memory_evictions'] == 0
|
|
assert stats['size'] == 1
|
|
assert stats['max_size'] == 5
|
|
|
|
# Vérifier que les stats mémoire sont incluses
|
|
assert 'current_bytes' in stats
|
|
assert 'current_mb' in stats
|
|
|
|
|
|
class TestMemoryManager:
|
|
"""Tests pour MemoryManager."""
|
|
|
|
def setup_method(self):
|
|
"""Setup pour chaque test."""
|
|
# Désactiver le monitoring pour les tests pour éviter les interférences
|
|
self.manager = MemoryManager(
|
|
max_memory_mb=100,
|
|
cleanup_threshold=0.8,
|
|
check_interval=60.0, # Intervalle long pour éviter les interférences
|
|
enable_monitoring=False # Désactiver pour les tests
|
|
)
|
|
|
|
def teardown_method(self):
|
|
"""Cleanup après chaque test."""
|
|
if hasattr(self, 'manager'):
|
|
try:
|
|
self.manager.shutdown()
|
|
except Exception:
|
|
pass # Ignorer les erreurs de cleanup
|
|
|
|
def test_resource_registration(self):
|
|
"""Test enregistrement de ressources."""
|
|
resource = {"data": "test"}
|
|
cleanup_func = MagicMock()
|
|
|
|
# Enregistrer
|
|
self.manager.register_resource(
|
|
"test_resource",
|
|
resource,
|
|
cleanup_func,
|
|
{"type": "test"}
|
|
)
|
|
|
|
# Vérifier
|
|
assert "test_resource" in self.manager.resource_registry
|
|
assert self.manager.resource_registry["test_resource"]["resource"] == resource
|
|
assert self.manager.resource_registry["test_resource"]["metadata"]["type"] == "test"
|
|
assert "test_resource" in self.manager.cleanup_functions
|
|
|
|
def test_resource_unregistration(self):
|
|
"""Test désenregistrement de ressources."""
|
|
resource = {"data": "test"}
|
|
|
|
# Enregistrer puis désenregistrer
|
|
self.manager.register_resource("test_resource", resource)
|
|
assert self.manager.unregister_resource("test_resource")
|
|
|
|
# Vérifier suppression
|
|
assert "test_resource" not in self.manager.resource_registry
|
|
|
|
# Désenregistrer inexistant
|
|
assert not self.manager.unregister_resource("nonexistent")
|
|
|
|
@patch('psutil.Process')
|
|
def test_memory_usage(self, mock_process):
|
|
"""Test mesure d'utilisation mémoire."""
|
|
# Mock psutil
|
|
mock_memory_info = MagicMock()
|
|
mock_memory_info.rss = 100 * 1024 * 1024 # 100MB
|
|
mock_process.return_value.memory_info.return_value = mock_memory_info
|
|
|
|
usage = self.manager.get_memory_usage()
|
|
assert usage == 100.0
|
|
|
|
@patch('psutil.Process')
|
|
def test_cleanup_triggered(self, mock_process):
|
|
"""Test déclenchement du nettoyage."""
|
|
# Mock mémoire élevée
|
|
mock_memory_info = MagicMock()
|
|
mock_memory_info.rss = 90 * 1024 * 1024 # 90MB > 80MB threshold
|
|
mock_process.return_value.memory_info.return_value = mock_memory_info
|
|
|
|
# Enregistrer une ressource ancienne
|
|
cleanup_func = MagicMock()
|
|
self.manager.register_resource("old_resource", {"data": "test"}, cleanup_func)
|
|
|
|
# Simuler ancienneté
|
|
old_time = datetime.now() - timedelta(hours=2)
|
|
self.manager.resource_registry["old_resource"]["last_accessed"] = old_time
|
|
|
|
# Déclencher nettoyage
|
|
stats = self.manager.cleanup_if_needed()
|
|
|
|
assert stats['cleanup_triggered']
|
|
assert stats['resources_cleaned'] == 1
|
|
cleanup_func.assert_called_once()
|
|
|
|
def test_stats(self):
|
|
"""Test statistiques du gestionnaire."""
|
|
# Enregistrer quelques ressources
|
|
for i in range(3):
|
|
self.manager.register_resource(f"resource{i}", {"data": i})
|
|
|
|
stats = self.manager.get_stats()
|
|
|
|
assert stats['max_memory_mb'] == 100
|
|
assert stats['registered_resources'] == 3
|
|
assert stats['cleanup_threshold'] == 0.8
|
|
assert stats['check_interval'] == 60.0 # Corrigé: était 1.0
|
|
assert not stats['running'] or not self.manager.enable_monitoring # Monitoring désactivé
|
|
|
|
def test_gpu_resource_management(self):
|
|
"""Test gestion des ressources GPU."""
|
|
# Créer un manager avec gestion GPU activée
|
|
manager = MemoryManager(
|
|
max_memory_mb=100,
|
|
enable_monitoring=False,
|
|
enable_gpu_management=True
|
|
)
|
|
|
|
try:
|
|
# Enregistrer une ressource GPU
|
|
def cleanup_gpu_model(resource_id):
|
|
# Simuler le nettoyage d'un modèle GPU
|
|
pass
|
|
|
|
manager.register_gpu_resource(
|
|
"test_model",
|
|
"model",
|
|
cleanup_gpu_model,
|
|
{"size_mb": 500}
|
|
)
|
|
|
|
# Vérifier l'enregistrement
|
|
assert "test_model" in manager._gpu_resources
|
|
assert "gpu_test_model" in manager.resource_registry
|
|
|
|
# Obtenir les stats GPU
|
|
gpu_usage = manager.get_gpu_memory_usage()
|
|
assert isinstance(gpu_usage, dict)
|
|
|
|
# Nettoyer les ressources GPU
|
|
cleaned = manager.cleanup_gpu_resources(max_age_hours=0.0) # Force cleanup
|
|
assert cleaned >= 0 # Peut être 0 si pas de GPU ou pas de ressources anciennes
|
|
|
|
# Désenregistrer
|
|
assert manager.unregister_gpu_resource("test_model")
|
|
assert "test_model" not in manager._gpu_resources
|
|
|
|
finally:
|
|
manager.shutdown()
|
|
|
|
def test_gpu_management_disabled(self):
|
|
"""Test comportement quand gestion GPU désactivée."""
|
|
manager = MemoryManager(
|
|
enable_monitoring=False,
|
|
enable_gpu_management=False
|
|
)
|
|
|
|
try:
|
|
# Tenter d'enregistrer une ressource GPU
|
|
manager.register_gpu_resource("test", "model")
|
|
|
|
# Ne doit pas être enregistrée
|
|
assert "test" not in manager._gpu_resources
|
|
|
|
# Stats GPU doivent indiquer que c'est désactivé
|
|
gpu_usage = manager.get_gpu_memory_usage()
|
|
assert not gpu_usage['available']
|
|
assert 'disabled' in gpu_usage['reason']
|
|
|
|
finally:
|
|
manager.shutdown()
|
|
"""Test arrêt propre."""
|
|
# Enregistrer des ressources avec cleanup
|
|
cleanup_funcs = []
|
|
for i in range(3):
|
|
cleanup_func = MagicMock()
|
|
cleanup_funcs.append(cleanup_func)
|
|
self.manager.register_resource(f"resource{i}", {"data": i}, cleanup_func)
|
|
|
|
# Arrêter
|
|
self.manager.shutdown()
|
|
|
|
# Vérifier que tous les cleanups ont été appelés
|
|
for cleanup_func in cleanup_funcs:
|
|
cleanup_func.assert_called_once()
|
|
|
|
# Vérifier état
|
|
assert not self.manager._running
|
|
assert len(self.manager.resource_registry) == 0
|
|
assert len(self.manager.cleanup_functions) == 0
|
|
|
|
def test_gpu_resource_management(self):
|
|
"""Test gestion des ressources GPU."""
|
|
# Créer un manager avec gestion GPU activée
|
|
manager = MemoryManager(
|
|
max_memory_mb=100,
|
|
enable_monitoring=False,
|
|
enable_gpu_management=True
|
|
)
|
|
|
|
try:
|
|
# Enregistrer une ressource GPU
|
|
def cleanup_gpu_model(resource_id):
|
|
# Simuler le nettoyage d'un modèle GPU
|
|
pass
|
|
|
|
manager.register_gpu_resource(
|
|
"test_model",
|
|
"model",
|
|
cleanup_gpu_model,
|
|
{"size_mb": 500}
|
|
)
|
|
|
|
# Vérifier l'enregistrement
|
|
if manager.enable_gpu_management: # Peut être désactivé si pas de GPU
|
|
assert "test_model" in manager._gpu_resources
|
|
assert "gpu_test_model" in manager.resource_registry
|
|
|
|
# Obtenir les stats GPU
|
|
gpu_usage = manager.get_gpu_memory_usage()
|
|
assert isinstance(gpu_usage, dict)
|
|
|
|
# Nettoyer les ressources GPU
|
|
cleaned = manager.cleanup_gpu_resources(max_age_hours=0.0) # Force cleanup
|
|
assert cleaned >= 0 # Peut être 0 si pas de GPU ou pas de ressources anciennes
|
|
|
|
# Désenregistrer
|
|
result = manager.unregister_gpu_resource("test_model")
|
|
# Peut être False si GPU management désactivé
|
|
|
|
finally:
|
|
manager.shutdown()
|
|
|
|
def test_gpu_management_disabled(self):
|
|
"""Test comportement quand gestion GPU désactivée."""
|
|
manager = MemoryManager(
|
|
enable_monitoring=False,
|
|
enable_gpu_management=False
|
|
)
|
|
|
|
try:
|
|
# Tenter d'enregistrer une ressource GPU
|
|
manager.register_gpu_resource("test", "model")
|
|
|
|
# Ne doit pas être enregistrée
|
|
assert "test" not in manager._gpu_resources
|
|
|
|
# Stats GPU doivent indiquer que c'est désactivé
|
|
gpu_usage = manager.get_gpu_memory_usage()
|
|
assert not gpu_usage['available']
|
|
assert 'disabled' in gpu_usage['reason']
|
|
|
|
finally:
|
|
manager.shutdown()
|
|
|
|
|
|
class TestGlobalMemoryManager:
|
|
"""Tests pour le gestionnaire global."""
|
|
|
|
def teardown_method(self):
|
|
"""Cleanup après chaque test."""
|
|
try:
|
|
shutdown_memory_manager()
|
|
except Exception:
|
|
pass # Ignorer les erreurs de cleanup
|
|
|
|
def test_singleton_behavior(self):
|
|
"""Test comportement singleton."""
|
|
manager1 = get_memory_manager()
|
|
manager2 = get_memory_manager()
|
|
|
|
assert manager1 is manager2
|
|
|
|
def test_shutdown_global(self):
|
|
"""Test arrêt du gestionnaire global."""
|
|
manager = get_memory_manager()
|
|
assert manager is not None
|
|
|
|
shutdown_memory_manager()
|
|
|
|
# Nouveau gestionnaire après shutdown
|
|
new_manager = get_memory_manager()
|
|
assert new_manager is not manager
|
|
|
|
|
|
class TestIntegration:
|
|
"""Tests d'intégration entre les composants."""
|
|
|
|
def setup_method(self):
|
|
"""Setup pour chaque test."""
|
|
self.cache = EffectiveLRUCache(
|
|
max_size=10,
|
|
max_memory_mb=2.0,
|
|
enable_monitoring=False
|
|
)
|
|
# Désactiver le monitoring pour le gestionnaire global aussi
|
|
self.manager = get_memory_manager(enable_monitoring=False)
|
|
|
|
def teardown_method(self):
|
|
"""Cleanup après chaque test."""
|
|
if hasattr(self, 'cache'):
|
|
try:
|
|
self.cache.stop_monitoring()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
shutdown_memory_manager()
|
|
except Exception:
|
|
pass
|
|
|
|
def test_cache_with_memory_manager(self):
|
|
"""Test intégration cache avec gestionnaire mémoire."""
|
|
# Enregistrer le cache dans le gestionnaire
|
|
self.manager.register_resource(
|
|
"test_cache",
|
|
self.cache,
|
|
lambda cache: cache.clear()
|
|
)
|
|
|
|
# Ajouter des données au cache
|
|
for i in range(5):
|
|
self.cache.put(f"key{i}", np.zeros(100, dtype=np.float32))
|
|
|
|
assert len(self.cache) == 5
|
|
|
|
# Simuler nettoyage
|
|
old_time = datetime.now() - timedelta(hours=2)
|
|
self.manager.resource_registry["test_cache"]["last_accessed"] = old_time
|
|
|
|
# Forcer nettoyage
|
|
cleaned = self.manager._cleanup_old_resources(max_age_hours=1.0)
|
|
|
|
assert cleaned == 1
|
|
assert len(self.cache) == 0 # Cache vidé par cleanup
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__]) |