"""Tests unitaires pour le module utils.graph_utils. Ces tests vérifient les fonctions d'extraction et de traitement des graphes. """ from unittest.mock import MagicMock, patch import networkx as nx import pandas as pd import pytest from utils.graph_utils import ( charger_graphe, couleur_noeud, determiner_couleur_par_seuil, extraire_chemins_depuis, extraire_chemins_vers, load_seuils_config, recuperer_donnees, recuperer_donnees_2, ) class TestExtraireCheminsDepuis: """Tests pour la fonction extraire_chemins_depuis.""" def test_chemin_simple(self, simple_graph): """Test l'extraction d'un chemin simple depuis un nœud.""" chemins = extraire_chemins_depuis(simple_graph, "ProduitA") assert len(chemins) > 0 # Vérifier qu'il existe au moins un chemin qui commence par ProduitA assert any(chemin[0] == "ProduitA" for chemin in chemins) def test_chemin_depuis_noeud_terminal(self, simple_graph): """Test l'extraction depuis un nœud sans successeurs.""" chemins = extraire_chemins_depuis(simple_graph, "Chine_geographique") assert len(chemins) == 1 assert chemins[0] == ["Chine_geographique"] def test_chemins_multiples(self, complex_graph): """Test l'extraction de multiples chemins depuis un nœud.""" chemins = extraire_chemins_depuis(complex_graph, "ProduitX") # ProduitX a 2 composants, chacun menant à au moins 1 minerai assert len(chemins) >= 3 # Y→Z1, Y→Z2, W→Z1 def test_detection_cycles(self): """Test que les cycles sont correctement évités.""" G = nx.DiGraph() G.add_edges_from([("A", "B"), ("B", "C"), ("C", "A")]) # Cycle chemins = extraire_chemins_depuis(G, "A") # Ne doit pas boucler infiniment, le résultat doit être une liste assert isinstance(chemins, list) # Vérifier qu'aucun chemin ne contient de doublons for chemin in chemins: assert len(chemin) == len(set(chemin)) def test_graphe_vide(self): """Test avec un graphe vide.""" G = nx.DiGraph() with pytest.raises(nx.NetworkXError): extraire_chemins_depuis(G, "noeud_inexistant") class TestExtraireCheminsVers: """Tests pour la fonction extraire_chemins_vers.""" def test_chemins_vers_cible(self, simple_graph): """Test l'extraction des chemins vers un nœud cible.""" chemins = extraire_chemins_vers(simple_graph, "Chine_geographique", niveau_demande=0) # Doit trouver au moins un chemin depuis ProduitA (niveau 0) assert len(chemins) > 0 assert all(chemin[-1] == "Chine_geographique" for chemin in chemins) def test_chemins_vers_avec_niveau_filtre(self, complex_graph): """Test que seuls les chemins avec le niveau demandé sont retournés.""" chemins = extraire_chemins_vers(complex_graph, "MineraiZ1", niveau_demande=0) # Tous les chemins doivent contenir un nœud de niveau 0 (ProduitX) assert len(chemins) > 0 for chemin in chemins: niveaux = [complex_graph.nodes[n].get("niveau", -1) for n in chemin] assert 0 in niveaux def test_chemins_vers_noeud_source(self, simple_graph): """Test vers un nœud sans prédécesseurs.""" chemins = extraire_chemins_vers(simple_graph, "ProduitA", niveau_demande=0) assert len(chemins) == 1 assert chemins[0] == ["ProduitA"] class TestRecupererDonnees: """Tests pour la fonction recuperer_donnees.""" def test_recuperation_donnees_valides(self, simple_graph): """Test la récupération de données depuis des nœuds valides.""" noeuds = ["Fabrication_ComposantB"] df = recuperer_donnees(simple_graph, noeuds) assert isinstance(df, pd.DataFrame) assert not df.empty assert "categorie" in df.columns assert "nom" in df.columns assert "ihh_pays" in df.columns def test_recuperation_avec_noeud_invalide(self, simple_graph, caplog): """Test la gestion d'un nœud avec format invalide.""" noeuds = ["NoeudInvalide"] # Pas de '_' dans le nom df = recuperer_donnees(simple_graph, noeuds) # Doit retourner un DataFrame vide ou partiel assert isinstance(df, pd.DataFrame) # Vérifier qu'un warning a été émis assert "Nom de nœud inattendu" in caplog.text or df.empty def test_calcul_ics_moyen(self): """Test le calcul de l'ICS moyen pour un minerai.""" G = nx.DiGraph() G.add_node("Traitement_MineraiTest") G.add_node("MineraiTest", niveau=2) G.add_node("CompA", niveau=1) G.add_node("CompB", niveau=1) G.add_edge("CompA", "MineraiTest", ics=0.6) G.add_edge("CompB", "MineraiTest", ics=0.8) noeuds = ["Traitement_MineraiTest"] df = recuperer_donnees(G, noeuds) # ICS moyen devrait être (60 + 80) / 2 = 70 if not df.empty: assert "ics_minerai" in df.columns class TestRecupererDonnees2: """Tests pour la fonction recuperer_donnees_2.""" def test_recuperation_donnees_minerais(self, complex_graph): """Test la récupération des données IVC/IHH pour les minerais.""" minerais = ["MineraiZ1"] donnees = recuperer_donnees_2(complex_graph, minerais) assert isinstance(donnees, list) assert len(donnees) == 1 assert donnees[0]["nom"] == "MineraiZ1" assert "ivc" in donnees[0] def test_minerai_manquant(self, simple_graph, caplog): """Test avec un minerai dont les nœuds sont manquants.""" minerais = ["MineraiInexistant"] with caplog.at_level("WARNING", logger="utils.graph_utils"): donnees = recuperer_donnees_2(simple_graph, minerais) # Doit retourner une liste vide et logger un warning assert isinstance(donnees, list) assert len(donnees) == 0 # Vérifier soit le caplog soit la fonction a fonctionné assert "manquants" in caplog.text.lower() or len(donnees) == 0 def test_minerai_avec_extraction_et_reserves(self): """Test avec un minerai ayant Extraction_ et Reserves_.""" G = nx.DiGraph() G.add_node("MineraiTest", niveau=2, ivc=50) G.add_node("Extraction_MineraiTest", niveau=10, ihh_pays=40) G.add_node("Reserves_MineraiTest", niveau=10, ihh_pays=60) minerais = ["MineraiTest"] donnees = recuperer_donnees_2(G, minerais) assert len(donnees) == 1 assert donnees[0]["ivc"] == 50 assert donnees[0]["ihh_extraction"] == 40 assert donnees[0]["ihh_reserves"] == 60 def test_minerai_extraction_manquante(self): """Test avec un minerai dont le nœud Extraction_ est manquant.""" G = nx.DiGraph() G.add_node("MineraiTest", niveau=2, ivc=50) G.add_node("Reserves_MineraiTest", niveau=10, ihh_pays=60) minerais = ["MineraiTest"] donnees = recuperer_donnees_2(G, minerais) # Le minerai doit être ignoré car Extraction_ manque assert len(donnees) == 0 def test_minerai_reserves_manquante(self): """Test avec un minerai dont le nœud Reserves_ est manquant.""" G = nx.DiGraph() G.add_node("MineraiTest", niveau=2, ivc=50) G.add_node("Extraction_MineraiTest", niveau=10, ihh_pays=40) minerais = ["MineraiTest"] donnees = recuperer_donnees_2(G, minerais) # Le minerai doit être ignoré car Reserves_ manque assert len(donnees) == 0 def test_erreur_exception_dans_boucle(self): """Test la gestion d'erreur dans la boucle de recuperer_donnees_2.""" G = nx.DiGraph() G.add_node("MineraiTest", niveau=2, ivc="invalide") G.add_node("Extraction_MineraiTest", niveau=10, ihh_pays=40) G.add_node("Reserves_MineraiTest", niveau=10, ihh_pays=60) minerais = ["MineraiTest"] donnees = recuperer_donnees_2(G, minerais) # ivc="invalide" va provoquer une ValueError dans int(), capturée par le except assert isinstance(donnees, list) class TestRecupererDonneesIcsCalcul: """Tests complémentaires pour le calcul ICS dans recuperer_donnees.""" def test_ics_calcul_avec_valeurs_non_vides(self): """Test le calcul ICS quand des valeurs existent sur les arêtes.""" G = nx.MultiDiGraph() G.add_node("Traitement_Cuivre", ihh_pays=0, ihh_acteurs=0) G.add_node("Cuivre", niveau=2) G.add_node("FabA", niveau=1) G.add_node("FabB", niveau=1) G.add_edge("FabA", "Cuivre", ics=0.4) G.add_edge("FabB", "Cuivre", ics=0.6) noeuds = ["Traitement_Cuivre"] df = recuperer_donnees(G, noeuds) assert not df.empty # ICS moyen = (40 + 60) / 2 = 50 assert df.iloc[0]["ics_minerai"] == 50 assert df.iloc[0]["ics_cat"] == 2 # 50 > 33 et 50 <= 66 def test_ics_cat_faible(self): """Test la catégorisation ICS pour une valeur faible (<=33).""" G = nx.MultiDiGraph() G.add_node("Traitement_Nickel", ihh_pays=10, ihh_acteurs=5) G.add_node("Nickel", niveau=2) G.add_node("Fab1", niveau=1) G.add_edge("Fab1", "Nickel", ics=0.2) noeuds = ["Traitement_Nickel"] df = recuperer_donnees(G, noeuds) assert not df.empty # ICS = 20, catégorie 1 (<=33) assert df.iloc[0]["ics_minerai"] == 20 assert df.iloc[0]["ics_cat"] == 1 def test_ics_cat_elevee(self): """Test la catégorisation ICS pour une valeur élevée (>66).""" G = nx.MultiDiGraph() G.add_node("Traitement_Or", ihh_pays=80, ihh_acteurs=60) G.add_node("Or", niveau=2) G.add_node("Fab1", niveau=1) G.add_edge("Fab1", "Or", ics=0.9) noeuds = ["Traitement_Or"] df = recuperer_donnees(G, noeuds) assert not df.empty # ICS = 90, catégorie 3 (>66) assert df.iloc[0]["ics_minerai"] == 90 assert df.iloc[0]["ics_cat"] == 3 def test_ics_sans_predecesseurs(self): """Test le calcul ICS quand il n'y a pas de prédécesseurs pour le minerai.""" G = nx.MultiDiGraph() G.add_node("Traitement_Fer", ihh_pays=30, ihh_acteurs=20) G.add_node("Fer", niveau=2) # Pas de prédécesseurs pour Fer noeuds = ["Traitement_Fer"] df = recuperer_donnees(G, noeuds) assert not df.empty # ICS par défaut = 50 (pas de valeurs calculées) assert df.iloc[0]["ics_minerai"] == 50 def test_ics_exception_criticite(self): """Test la gestion d'erreur lors du calcul de criticité.""" G = nx.MultiDiGraph() G.add_node("Traitement_Zinc", ihh_pays=25, ihh_acteurs=15) # Le nœud Zinc n'existe PAS dans le graphe, donc graph.predecessors() va échouer # Mais le minerai est quand même extrait dans la 2ème boucle noeuds = ["Traitement_Zinc"] df = recuperer_donnees(G, noeuds) # L'exception est attrapée, ics par défaut = 50 assert isinstance(df, pd.DataFrame) class TestLoadSeuilsConfig: """Tests pour la fonction load_seuils_config.""" def test_chargement_fichier_valide(self, sample_config_yaml): """Test le chargement d'un fichier YAML valide.""" seuils = load_seuils_config(str(sample_config_yaml)) assert "ISG" in seuils assert "IHH" in seuils assert "IVC" in seuils assert seuils["ISG"]["vert"]["max"] == 40 assert seuils["IHH"]["rouge"]["min"] == 25 def test_chargement_fichier_inexistant(self): """Test le fallback quand le fichier n'existe pas.""" seuils = load_seuils_config("/chemin/inexistant/config.yaml") # Doit retourner les valeurs par défaut assert "ISG" in seuils assert "IHH" in seuils assert "IVC" in seuils assert seuils["ISG"]["vert"]["max"] == 40 assert seuils["IHH"]["vert"]["max"] == 15 assert seuils["IVC"]["rouge"]["min"] == 60 def test_chargement_fichier_yaml_invalide(self, tmp_path): """Test le fallback quand le fichier YAML est mal formé.""" bad_yaml = tmp_path / "bad_config.yaml" bad_yaml.write_text("{{{{invalide yaml!!!!}", encoding="utf-8") seuils = load_seuils_config(str(bad_yaml)) # Doit retourner les valeurs par défaut assert "ISG" in seuils assert "IHH" in seuils def test_chargement_fichier_sans_cle_seuils(self, tmp_path): """Test avec un YAML valide mais sans la clé 'seuils'.""" yaml_sans_seuils = tmp_path / "no_seuils.yaml" yaml_sans_seuils.write_text("autre_cle: valeur\n", encoding="utf-8") seuils = load_seuils_config(str(yaml_sans_seuils)) # data.get("seuils", {}) retourne {} car la clé n'existe pas assert seuils == {} class TestDeterminerCouleurParSeuil: """Tests pour la fonction determiner_couleur_par_seuil.""" @pytest.fixture def seuils_isg(self): """Seuils ISG standards pour les tests.""" return { "vert": {"max": 40}, "orange": {"min": 40, "max": 70}, "rouge": {"min": 70} } @pytest.fixture def seuils_ihh(self): """Seuils IHH standards pour les tests.""" return { "vert": {"max": 15}, "orange": {"min": 15, "max": 25}, "rouge": {"min": 25} } def test_valeur_negative_retourne_gris(self, seuils_isg): """Test qu'une valeur négative retourne 'gray'.""" assert determiner_couleur_par_seuil(-1, seuils_isg) == "gray" assert determiner_couleur_par_seuil(-100, seuils_isg) == "gray" def test_valeur_zone_verte(self, seuils_isg): """Test qu'une valeur dans la zone verte retourne 'darkgreen'.""" assert determiner_couleur_par_seuil(0, seuils_isg) == "darkgreen" assert determiner_couleur_par_seuil(20, seuils_isg) == "darkgreen" assert determiner_couleur_par_seuil(39, seuils_isg) == "darkgreen" def test_valeur_zone_orange(self, seuils_isg): """Test qu'une valeur dans la zone orange retourne 'orange'.""" assert determiner_couleur_par_seuil(40, seuils_isg) == "orange" assert determiner_couleur_par_seuil(55, seuils_isg) == "orange" assert determiner_couleur_par_seuil(69, seuils_isg) == "orange" def test_valeur_zone_rouge(self, seuils_isg): """Test qu'une valeur dans la zone rouge retourne 'darkred'.""" assert determiner_couleur_par_seuil(70, seuils_isg) == "darkred" assert determiner_couleur_par_seuil(100, seuils_isg) == "darkred" def test_valeur_limite_vert_orange(self, seuils_ihh): """Test la limite exacte entre vert et orange.""" # Valeur 14 juste sous le seuil orange, donc vert assert determiner_couleur_par_seuil(14, seuils_ihh) == "darkgreen" # Valeur 15 au seuil orange, donc orange assert determiner_couleur_par_seuil(15, seuils_ihh) == "orange" def test_valeur_limite_orange_rouge(self, seuils_ihh): """Test la limite exacte entre orange et rouge.""" # Valeur 24 juste sous le seuil rouge, donc orange assert determiner_couleur_par_seuil(24, seuils_ihh) == "orange" # Valeur 25 au seuil rouge, donc rouge assert determiner_couleur_par_seuil(25, seuils_ihh) == "darkred" def test_seuils_incomplets_sans_vert(self): """Test avec des seuils sans la clé 'vert'.""" seuils = { "orange": {"min": 40, "max": 70}, "rouge": {"min": 70} } # Valeur en zone rouge assert determiner_couleur_par_seuil(80, seuils) == "darkred" # Valeur en zone orange assert determiner_couleur_par_seuil(50, seuils) == "orange" # Valeur basse : pas de vert défini, tombe dans le défaut orange assert determiner_couleur_par_seuil(10, seuils) == "orange" def test_seuils_incomplets_sans_rouge(self): """Test avec des seuils sans la clé 'rouge'.""" seuils = { "vert": {"max": 40}, "orange": {"min": 40, "max": 70}, } assert determiner_couleur_par_seuil(10, seuils) == "darkgreen" assert determiner_couleur_par_seuil(50, seuils) == "orange" # Valeur haute sans seuil rouge : tombe dans le défaut orange assert determiner_couleur_par_seuil(80, seuils) == "orange" def test_seuils_vides(self): """Test avec un dictionnaire de seuils vide.""" assert determiner_couleur_par_seuil(50, {}) == "orange" assert determiner_couleur_par_seuil(0, {}) == "orange" def test_seuils_sans_min_dans_orange(self): """Test avec 'orange' présent mais sans clé 'min'.""" seuils = { "vert": {"max": 40}, "orange": {"max": 70}, "rouge": {"min": 70} } # La condition orange a besoin de min ET max, donc skip orange # Valeur 50 : pas rouge (sous 70), pas orange (manque min), pas vert (au dessus de 40) # Tombe dans le défaut orange assert determiner_couleur_par_seuil(50, seuils) == "orange" # Valeur 10 : sous le seuil vert (40), donc darkgreen assert determiner_couleur_par_seuil(10, seuils) == "darkgreen" def test_valeur_zero(self, seuils_isg): """Test avec la valeur zéro.""" assert determiner_couleur_par_seuil(0, seuils_isg) == "darkgreen" class TestCouleurNoeud: """Tests pour la fonction couleur_noeud.""" @pytest.fixture def config_yaml(self): """Retourne les seuils de configuration pour les tests.""" return { "ISG": {"vert": {"max": 40}, "orange": {"min": 40, "max": 70}, "rouge": {"min": 70}}, "IHH": {"vert": {"max": 15}, "orange": {"min": 15, "max": 25}, "rouge": {"min": 25}}, "IVC": {"vert": {"max": 15}, "orange": {"min": 15, "max": 60}, "rouge": {"min": 60}} } def test_pays_geographique_isg_vert(self, config_yaml): """Test couleur d'un pays géographique avec ISG faible (vert).""" G = nx.DiGraph() G.add_node("Australie_geographique", niveau=99, isg=25) niveaux = {"Australie_geographique": 99} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Australie_geographique", niveaux, G) assert couleur == "darkgreen" def test_pays_geographique_isg_orange(self, config_yaml): """Test couleur d'un pays géographique avec ISG moyen (orange).""" G = nx.DiGraph() G.add_node("Chine_geographique", niveau=99, isg=54) niveaux = {"Chine_geographique": 99} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Chine_geographique", niveaux, G) assert couleur == "orange" def test_pays_geographique_isg_rouge(self, config_yaml): """Test couleur d'un pays géographique avec ISG élevé (rouge).""" G = nx.DiGraph() G.add_node("RDC_geographique", niveau=99, isg=85) niveaux = {"RDC_geographique": 99} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("RDC_geographique", niveaux, G) assert couleur == "darkred" def test_pays_geographique_sans_isg(self, config_yaml): """Test couleur d'un pays géographique sans attribut ISG.""" G = nx.DiGraph() G.add_node("Inconnu_geographique", niveau=99) niveaux = {"Inconnu_geographique": 99} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Inconnu_geographique", niveaux, G) # isg = -1 par défaut, donc gray assert couleur == "gray" def test_pays_operation_niveau_11_connecte_pays(self, config_yaml): """Test couleur d'un nœud niveau 11 connecté à un pays géographique.""" G = nx.DiGraph() G.add_node("Chine_Fabrication_Batterie", niveau=11) G.add_node("Chine_geographique", niveau=99, isg=54) G.add_edge("Chine_Fabrication_Batterie", "Chine_geographique") niveaux = {"Chine_Fabrication_Batterie": 11, "Chine_geographique": 99} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Chine_Fabrication_Batterie", niveaux, G) assert couleur == "orange" def test_pays_operation_niveau_12(self, config_yaml): """Test couleur d'un nœud niveau 12 connecté à un pays géographique.""" G = nx.DiGraph() G.add_node("Australie_Reserves_Lithium", niveau=12) G.add_node("Australie_geographique", niveau=99, isg=25) G.add_edge("Australie_Reserves_Lithium", "Australie_geographique") niveaux = {"Australie_Reserves_Lithium": 12, "Australie_geographique": 99} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Australie_Reserves_Lithium", niveaux, G) assert couleur == "darkgreen" def test_pays_operation_niveau_1011(self, config_yaml): """Test couleur d'un nœud niveau 1011 connecté à un pays géographique.""" G = nx.DiGraph() G.add_node("Noeud_1011", niveau=1011) G.add_node("Pays_geographique", niveau=99, isg=80) G.add_edge("Noeud_1011", "Pays_geographique") niveaux = {"Noeud_1011": 1011, "Pays_geographique": 99} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Noeud_1011", niveaux, G) assert couleur == "darkred" def test_pays_operation_niveau_1012(self, config_yaml): """Test couleur d'un nœud niveau 1012 connecté à un pays géographique.""" G = nx.DiGraph() G.add_node("Noeud_1012", niveau=1012) G.add_node("Pays_geo", niveau=99, isg=10) G.add_edge("Noeud_1012", "Pays_geo") niveaux = {"Noeud_1012": 1012, "Pays_geo": 99} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Noeud_1012", niveaux, G) assert couleur == "darkgreen" def test_pays_operation_niveau_11_sans_pays_geo(self, config_yaml): """Test couleur d'un nœud niveau 11 sans successeur pays géographique.""" G = nx.DiGraph() G.add_node("Chine_Fabrication", niveau=11) G.add_node("Autre_Noeud", niveau=10) G.add_edge("Chine_Fabrication", "Autre_Noeud") niveaux = {"Chine_Fabrication": 11, "Autre_Noeud": 10} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Chine_Fabrication", niveaux, G) # Pas de successeur niveau 99, tombe dans lightblue assert couleur == "lightblue" def test_pays_operation_niveau_11_pays_geo_sans_isg(self, config_yaml): """Test couleur d'un nœud niveau 11 connecté à un pays sans ISG.""" G = nx.DiGraph() G.add_node("Noeud_11", niveau=11) G.add_node("Pays_geo", niveau=99) # Pas d'attribut isg G.add_edge("Noeud_11", "Pays_geo") niveaux = {"Noeud_11": 11, "Pays_geo": 99} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Noeud_11", niveaux, G) # isg = -1, donc gray assert couleur == "gray" def test_operation_niveau_10_ihh_vert(self, config_yaml): """Test couleur d'une opération niveau 10 avec IHH faible.""" G = nx.DiGraph() G.add_node("Fabrication_Test", niveau=10, ihh_pays=10) niveaux = {"Fabrication_Test": 10} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Fabrication_Test", niveaux, G) assert couleur == "darkgreen" def test_operation_niveau_10_ihh_orange(self, config_yaml): """Test couleur d'une opération niveau 10 avec IHH moyen.""" G = nx.DiGraph() G.add_node("Fabrication_Test", niveau=10, ihh_pays=20) niveaux = {"Fabrication_Test": 10} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Fabrication_Test", niveaux, G) assert couleur == "orange" def test_operation_niveau_10_ihh_rouge(self, config_yaml): """Test couleur d'une opération niveau 10 avec IHH élevé.""" G = nx.DiGraph() G.add_node("Fabrication_Test", niveau=10, ihh_pays=30) niveaux = {"Fabrication_Test": 10} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Fabrication_Test", niveaux, G) assert couleur == "darkred" def test_operation_niveau_1010(self, config_yaml): """Test couleur d'une opération niveau 1010 avec IHH.""" G = nx.DiGraph() G.add_node("Operation_1010", niveau=1010, ihh_pays=20) niveaux = {"Operation_1010": 1010} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Operation_1010", niveaux, G) assert couleur == "orange" def test_operation_niveau_10_sans_ihh(self, config_yaml): """Test couleur d'une opération niveau 10 sans attribut ihh_pays.""" G = nx.DiGraph() G.add_node("Fabrication_Test", niveau=10) niveaux = {"Fabrication_Test": 10} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Fabrication_Test", niveaux, G) # Pas d'ihh_pays, tombe dans lightblue assert couleur == "lightblue" def test_minerai_niveau_2_ivc_vert(self, config_yaml): """Test couleur d'un minerai niveau 2 avec IVC faible.""" G = nx.DiGraph() G.add_node("Lithium", niveau=2, ivc=10) niveaux = {"Lithium": 2} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Lithium", niveaux, G) assert couleur == "darkgreen" def test_minerai_niveau_2_ivc_orange(self, config_yaml): """Test couleur d'un minerai niveau 2 avec IVC moyen.""" G = nx.DiGraph() G.add_node("Cobalt", niveau=2, ivc=30) niveaux = {"Cobalt": 2} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Cobalt", niveaux, G) assert couleur == "orange" def test_minerai_niveau_2_ivc_rouge(self, config_yaml): """Test couleur d'un minerai niveau 2 avec IVC élevé.""" G = nx.DiGraph() G.add_node("Indium", niveau=2, ivc=65) niveaux = {"Indium": 2} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("Indium", niveaux, G) assert couleur == "darkred" def test_minerai_niveau_2_sans_ivc(self, config_yaml): """Test couleur d'un minerai niveau 2 sans attribut IVC.""" G = nx.DiGraph() G.add_node("MineraiSansIvc", niveau=2) niveaux = {"MineraiSansIvc": 2} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("MineraiSansIvc", niveaux, G) # Pas d'ivc, tombe dans lightblue assert couleur == "lightblue" def test_noeud_sans_niveau_connu(self, config_yaml): """Test couleur d'un nœud avec un niveau inconnu.""" G = nx.DiGraph() G.add_node("NoeudInconnu", niveau=5) niveaux = {"NoeudInconnu": 5} with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("NoeudInconnu", niveaux, G) assert couleur == "lightblue" def test_noeud_absent_du_dictionnaire_niveaux(self, config_yaml): """Test couleur d'un nœud absent du dictionnaire des niveaux.""" G = nx.DiGraph() G.add_node("NoeudOrphelin", isg=50) niveaux = {} # Pas de niveau défini with patch("utils.graph_utils.load_seuils_config", return_value=config_yaml): couleur = couleur_noeud("NoeudOrphelin", niveaux, G) # niveau par défaut = 99, isg=50 -> orange assert couleur == "orange" def test_operation_niveau_10_ihh_fallback_sans_cle_ihh(self): """Test le fallback IHH quand la clé IHH n'est pas dans les seuils.""" G = nx.DiGraph() G.add_node("Fabrication_Test", niveau=10, ihh_pays=20) niveaux = {"Fabrication_Test": 10} seuils_sans_ihh = { "ISG": {"vert": {"max": 40}, "orange": {"min": 40, "max": 70}, "rouge": {"min": 70}}, "IVC": {"vert": {"max": 15}, "orange": {"min": 15, "max": 60}, "rouge": {"min": 60}} } with patch("utils.graph_utils.load_seuils_config", return_value=seuils_sans_ihh): couleur = couleur_noeud("Fabrication_Test", niveaux, G) # Fallback: 20 <= 25 -> orange assert couleur == "orange" def test_operation_niveau_10_ihh_fallback_vert(self): """Test le fallback IHH pour une valeur faible (<=15).""" G = nx.DiGraph() G.add_node("Fabrication_Test", niveau=10, ihh_pays=10) niveaux = {"Fabrication_Test": 10} with patch("utils.graph_utils.load_seuils_config", return_value={}): couleur = couleur_noeud("Fabrication_Test", niveaux, G) assert couleur == "darkgreen" def test_operation_niveau_10_ihh_fallback_rouge(self): """Test le fallback IHH pour une valeur élevée (>25).""" G = nx.DiGraph() G.add_node("Fabrication_Test", niveau=10, ihh_pays=30) niveaux = {"Fabrication_Test": 10} with patch("utils.graph_utils.load_seuils_config", return_value={}): couleur = couleur_noeud("Fabrication_Test", niveaux, G) assert couleur == "darkred" def test_minerai_niveau_2_ivc_fallback_sans_cle_ivc(self): """Test le fallback IVC quand la clé IVC n'est pas dans les seuils.""" G = nx.DiGraph() G.add_node("Lithium", niveau=2, ivc=20) niveaux = {"Lithium": 2} seuils_sans_ivc = { "ISG": {"vert": {"max": 40}, "orange": {"min": 40, "max": 70}, "rouge": {"min": 70}}, } with patch("utils.graph_utils.load_seuils_config", return_value=seuils_sans_ivc): couleur = couleur_noeud("Lithium", niveaux, G) # Fallback IVC: 20 > 15 et <= 30 -> orange assert couleur == "orange" def test_minerai_niveau_2_ivc_fallback_vert(self): """Test le fallback IVC pour une valeur faible (<=15).""" G = nx.DiGraph() G.add_node("Lithium", niveau=2, ivc=10) niveaux = {"Lithium": 2} with patch("utils.graph_utils.load_seuils_config", return_value={}): couleur = couleur_noeud("Lithium", niveaux, G) assert couleur == "darkgreen" def test_minerai_niveau_2_ivc_fallback_rouge(self): """Test le fallback IVC pour une valeur élevée (>30).""" G = nx.DiGraph() G.add_node("Lithium", niveau=2, ivc=50) niveaux = {"Lithium": 2} with patch("utils.graph_utils.load_seuils_config", return_value={}): couleur = couleur_noeud("Lithium", niveaux, G) assert couleur == "darkred" class TestChargerGraphe: """Tests pour la fonction charger_graphe.""" def test_charger_graphe_deja_en_session(self): """Test que le graphe n'est pas rechargé s'il est déjà en session.""" mock_graph = nx.DiGraph() mock_session = {"G_temp": mock_graph, "G_temp_ivc": mock_graph.copy()} with patch("utils.graph_utils.st") as mock_st: mock_st.session_state = mock_session resultat = charger_graphe() assert resultat is True def test_charger_graphe_succes(self, tmp_path): """Test le chargement réussi d'un graphe DOT.""" dot_file = tmp_path / "test.dot" dot_file.write_text('digraph { A -> B; }', encoding="utf-8") mock_session = {} mock_graph = nx.MultiDiGraph() mock_graph.add_edge("A", "B") with patch("utils.graph_utils.st") as mock_st, \ patch("utils.graph_utils.charger_schema_depuis_gitea", return_value=True), \ patch("utils.graph_utils.read_dot", return_value=mock_graph), \ patch("utils.graph_utils.DOT_FILE", str(dot_file)): mock_st.session_state = mock_session resultat = charger_graphe() assert resultat is True assert "G_temp" in mock_session assert "G_temp_ivc" in mock_session def test_charger_graphe_gitea_echoue(self): """Test quand le chargement depuis Gitea échoue.""" mock_session = {} with patch("utils.graph_utils.st") as mock_st, \ patch("utils.graph_utils.charger_schema_depuis_gitea", return_value=False): mock_st.session_state = mock_session mock_st.error = MagicMock() resultat = charger_graphe() assert resultat is False mock_st.error.assert_called() def test_charger_graphe_exception_lecture_dot(self): """Test quand la lecture du fichier DOT lève une exception.""" mock_session = {} with patch("utils.graph_utils.st") as mock_st, \ patch("utils.graph_utils.charger_schema_depuis_gitea", return_value=True), \ patch("utils.graph_utils.read_dot", side_effect=Exception("Fichier corrompu")): mock_st.session_state = mock_session mock_st.error = MagicMock() resultat = charger_graphe() assert resultat is False # Vérifie que st.error a été appelé au moins une fois assert mock_st.error.call_count >= 1