| """ |
| Tests unitaires de l'interface Gradio (app2.py). |
| Usage : |
| pytest tests/test_app.py -v |
| ou depuis le dossier tests/ : python3 -m pytest test_app.py -v |
| """ |
| import sys |
| import os |
| import pytest |
| import numpy as np |
| import matplotlib |
| matplotlib.use('Agg') |
| import matplotlib.pyplot as plt |
| import threading |
| from unittest.mock import patch, MagicMock |
|
|
| |
| |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
| |
| from Endoregulated_AI_v27 import EndoRegulatedCore |
|
|
| |
| |
| import app |
| from app import text_to_embedding, visualize_embedding |
|
|
| |
| |
| |
|
|
| @pytest.fixture |
| def reset_core(): |
| """ |
| Réinitialise le core AVANT chaque test pour garantir l'indépendance. |
| IMPORTANT : on remplace le core DU MODULE app, pas celui de test_app. |
| """ |
| from Endoregulated_AI_v27 import EndoRegulatedCore, get_core_lock |
| |
| |
| new_core = EndoRegulatedCore(noise_level=0.15, seed=42) |
| |
| |
| app.core = new_core |
| app.core_lock = get_core_lock() |
| |
| yield new_core |
| |
| |
| new_core.input_counter = 0 |
|
|
|
|
| @pytest.fixture |
| def sample_texts(): |
| """Textes de test variés.""" |
| return [ |
| "Bonjour le monde", |
| "L'intelligence artificielle est fascinante", |
| "Le cycle Wuxing gouverne l'équilibre", |
| "Sheng et Ke dansent dans le chaos", |
| "La topologie des attracteurs révèle l'harmonie", |
| ] |
|
|
| |
| |
| |
|
|
| class TestTextToEmbedding: |
| """Tests de la fonction text_to_embedding.""" |
|
|
| def test_returns_tuple_of_three(self, reset_core): |
| """text_to_embedding retourne un tuple de 3 éléments.""" |
| result = text_to_embedding("test") |
| assert isinstance(result, tuple) |
| assert len(result) == 3 |
|
|
| def test_embedding_20d_shape(self, reset_core): |
| """L'embedding 20D a la bonne forme.""" |
| emb_20d, emb_768d, attractor = text_to_embedding("test") |
| assert emb_20d.shape == (20,) |
| assert emb_20d.dtype == np.float32 |
|
|
| def test_embedding_768d_shape(self, reset_core): |
| """L'embedding 768D a la bonne forme.""" |
| emb_20d, emb_768d, attractor = text_to_embedding("test") |
| assert emb_768d.shape == (768,) |
| assert emb_768d.dtype == np.float32 |
|
|
| def test_attractor_is_valid(self, reset_core): |
| """L'attracteur retourné est valide (A-T).""" |
| emb_20d, emb_768d, attractor = text_to_embedding("test") |
| assert attractor in reset_core.attractors |
| assert len(attractor) == 1 |
| assert 'A' <= attractor <= 'T' |
|
|
| def test_embedding_values_in_range(self, reset_core): |
| """Les valeurs de l'embedding 20D sont dans [-1, 1].""" |
| emb_20d, _, _ = text_to_embedding("test") |
| assert np.all(emb_20d >= -1.0) |
| assert np.all(emb_20d <= 1.0) |
|
|
| def test_deterministic_same_text(self, reset_core): |
| """ |
| Le même texte produit le même embedding SI l'état du core est identique. |
| """ |
| from Endoregulated_AI_v27 import EndoRegulatedCore |
| |
| core1 = EndoRegulatedCore(noise_level=0.15, seed=42) |
| core2 = EndoRegulatedCore(noise_level=0.15, seed=42) |
| |
| original_core = app.core |
| original_lock = app.core_lock |
| |
| try: |
| app.core = core1 |
| app.core_lock = app.get_core_lock() |
| emb1, emb768_1, attr1 = text_to_embedding("bonjour") |
| |
| app.core = core2 |
| emb2, emb768_2, attr2 = text_to_embedding("bonjour") |
| |
| np.testing.assert_array_almost_equal(emb1, emb2) |
| np.testing.assert_array_almost_equal(emb768_1, emb768_2) |
| assert attr1 == attr2 |
| finally: |
| app.core = original_core |
| app.core_lock = original_lock |
|
|
| def test_different_texts_different_embeddings(self, reset_core): |
| """Des textes différents produisent des embeddings différents.""" |
| emb1, _, _ = text_to_embedding("chat") |
| emb2, _, _ = text_to_embedding("chien") |
| |
| assert not np.allclose(emb1, emb2) |
|
|
| def test_core_state_changes(self, reset_core): |
| """L'injection dans le core modifie son état.""" |
| initial_counter = reset_core.input_counter |
| text_to_embedding("test") |
| assert reset_core.input_counter == initial_counter + 1 |
|
|
| def test_multiple_injections_evolve_system(self, reset_core): |
| """Plusieurs injections font évoluer le système.""" |
| for text in ["texte1", "texte2", "texte3", "texte4", "texte5"]: |
| text_to_embedding(text) |
| assert True |
|
|
|
|
| |
| |
| |
|
|
| class TestVisualizeEmbedding: |
| """Tests de la fonction visualize_embedding.""" |
|
|
| def test_returns_tuple_of_three(self, reset_core): |
| """visualize_embedding retourne un tuple de 3 éléments.""" |
| result = visualize_embedding("test", [], "fr") |
| assert isinstance(result, tuple) |
| assert len(result) == 3 |
|
|
| def test_returns_figure_and_text(self, reset_core): |
| """Retourne une figure matplotlib et du texte.""" |
| fig, info_text, cache = visualize_embedding("test", [], "fr") |
| |
| assert fig is not None or info_text is not None |
| assert isinstance(info_text, str) |
| assert isinstance(cache, list) |
|
|
| def test_info_text_contains_metrics(self, reset_core): |
| """Le texte d'info contient les métriques.""" |
| fig, info_text, cache = visualize_embedding("test", [], "fr") |
| |
| assert "**Attracteur :**" in info_text |
| assert "**Asymétrie η :**" in info_text |
| assert "**Frustration E :**" in info_text |
| assert "**Seuil R :**" in info_text |
| assert "**Régime :**" in info_text |
| assert "**Embedding 20D :**" in info_text |
| assert "**Embedding 768D :**" in info_text |
| assert "**Taux de compression :**" in info_text |
|
|
| def test_cache_grows_with_calls(self, reset_core): |
| """Le cache grandit avec les appels successifs.""" |
| cache = [] |
| |
| for text in ["texte1", "texte2", "texte3"]: |
| fig, info, cache = visualize_embedding(text, cache, "fr") |
| |
| assert len(cache) == 3 |
|
|
| def test_cache_respects_max_size(self, reset_core): |
| """Le cache respecte la taille maximale (20).""" |
| cache = [] |
| |
| for i in range(25): |
| fig, info, cache = visualize_embedding(f"texte{i}", cache, "fr") |
| |
| assert len(cache) <= 20 |
|
|
| def test_pca_activates_after_5_points(self, reset_core): |
| """La PCA s'active après 5 points.""" |
| cache = [] |
| |
| for i in range(5): |
| fig, info, cache = visualize_embedding(f"texte{i}", cache, "fr") |
| |
| |
| assert fig is not None or "PCA" in str(info) |
|
|
| def test_empty_text_returns_warning(self, reset_core): |
| """Un texte vide retourne un avertissement.""" |
| fig, info_text, cache = visualize_embedding("", [], "fr") |
| |
| assert fig is None |
| assert "⚠️" in info_text or "Veuillez entrer" in info_text |
|
|
| def test_whitespace_only_returns_warning(self, reset_core): |
| """Un texte avec seulement des espaces retourne un avertissement.""" |
| fig, info_text, cache = visualize_embedding(" \t\n ", [], "fr") |
| |
| assert fig is None |
| assert "⚠️" in info_text or "Veuillez entrer" in info_text |
|
|
| def test_long_text_is_truncated_in_info(self, reset_core): |
| """Un texte long est tronqué dans l'info.""" |
| long_text = "a" * 100 |
| fig, info_text, cache = visualize_embedding(long_text, [], "fr") |
| |
| assert "..." in info_text or long_text[:50] in info_text |
|
|
|
|
| |
| |
| |
|
|
| class TestEdgeCases: |
| """Tests de cas limites et caractères spéciaux.""" |
|
|
| def test_unicode_characters(self, reset_core): |
| """Gère les caractères Unicode.""" |
| emb, _, _ = text_to_embedding("日本語テスト 🎉") |
| assert emb.shape == (20,) |
|
|
| def test_emoji(self, reset_core): |
| """Gère les emojis.""" |
| emb, _, _ = text_to_embedding("Hello 👋 World 🌍") |
| assert emb.shape == (20,) |
|
|
| def test_special_characters(self, reset_core): |
| """Gère les caractères spéciaux.""" |
| emb, _, _ = text_to_embedding("!@#$%^&*()_+-=[]{}|;':\",./<>?") |
| assert emb.shape == (20,) |
|
|
| def test_newlines_and_tabs(self, reset_core): |
| """Gère les sauts de ligne et tabulations.""" |
| emb, _, _ = text_to_embedding("ligne1\nligne2\ttab") |
| assert emb.shape == (20,) |
|
|
| def test_very_long_text(self, reset_core): |
| """Gère les textes très longs.""" |
| long_text = "a" * 10000 |
| emb, _, _ = text_to_embedding(long_text) |
| assert emb.shape == (20,) |
|
|
| def test_single_character(self, reset_core): |
| """Gère un caractère unique.""" |
| emb, _, _ = text_to_embedding("a") |
| assert emb.shape == (20,) |
|
|
| def test_repeated_characters(self, reset_core): |
| """Gère les caractères répétés.""" |
| emb, _, _ = text_to_embedding("aaaaaaaaaa") |
| assert emb.shape == (20,) |
|
|
|
|
| |
| |
| |
|
|
| class TestThreadSafety: |
| """Tests de la thread safety de l'interface.""" |
|
|
| def test_concurrent_text_to_embedding(self, reset_core): |
| """Plusieurs threads peuvent appeler text_to_embedding simultanément.""" |
| errors = [] |
| results = [] |
| |
| def worker(text): |
| try: |
| emb, _, _ = text_to_embedding(text) |
| results.append(emb) |
| except Exception as e: |
| errors.append(e) |
| |
| threads = [ |
| threading.Thread(target=worker, args=(f"texte{i}",)) |
| for i in range(10) |
| ] |
| |
| for t in threads: |
| t.start() |
| for t in threads: |
| t.join() |
| |
| assert not errors, f"Erreurs thread : {errors}" |
| assert len(results) == 10 |
|
|
| def test_concurrent_visualize_embedding(self, reset_core): |
| """Plusieurs threads peuvent appeler visualize_embedding simultanément.""" |
| errors = [] |
| |
| def worker(text, cache): |
| try: |
| visualize_embedding(text, cache, "fr") |
| except Exception as e: |
| errors.append(e) |
| |
| threads = [ |
| threading.Thread(target=worker, args=(f"texte{i}", [])) |
| for i in range(5) |
| ] |
| |
| for t in threads: |
| t.start() |
| for t in threads: |
| t.join() |
| |
| assert not errors, f"Erreurs thread : {errors}" |
|
|
| def test_lock_protects_core(self, reset_core): |
| """Le lock protège l'accès au core.""" |
| initial_counter = reset_core.input_counter |
| |
| def worker(): |
| for _ in range(10): |
| text_to_embedding("test") |
| |
| threads = [threading.Thread(target=worker) for _ in range(5)] |
| |
| for t in threads: |
| t.start() |
| for t in threads: |
| t.join() |
| |
| assert reset_core.input_counter == initial_counter + 50 |
|
|
|
|
| |
| |
| |
|
|
| class TestIntegration: |
| """Tests d'intégration combinant plusieurs composants.""" |
|
|
| def test_full_pipeline(self, reset_core, sample_texts): |
| """Pipeline complet : plusieurs textes → visualisation.""" |
| cache = [] |
| |
| for text in sample_texts: |
| fig, info, cache = visualize_embedding(text, cache, "fr") |
| |
| assert len(cache) == len(sample_texts) |
| assert "Attracteur" in info |
|
|
| def test_compression_ratio_displayed(self, reset_core): |
| """Le taux de compression est affiché dans l'info.""" |
| fig, info_text, cache = visualize_embedding("test", [], "fr") |
| |
| assert "**Taux de compression :**" in info_text |
| import re |
| match = re.search(r'Taux de compression :\*\* ([\d.]+)x', info_text) |
| if match: |
| ratio = float(match.group(1)) |
| |
| assert 35 <= ratio <= 45, f"Taux de compression inattendu : {ratio}" |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| pytest.main([__file__, "-v"]) |
|
|