Code/tests/unit/test_graph_utils.py
Stéphan Peccini 8e2556c2b0
test(unit): +381 tests unitaires — couverture 16%→35%
- 9 nouveaux fichiers de tests (persistance, translations, fiches, indices, IHH)
- Enrichissement des tests existants (graph_utils, gitea, widgets, tickets)
- 67→448 tests, tous passent

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 11:52:21 +01:00

856 lines
33 KiB
Python

"""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