- 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>
477 lines
17 KiB
Python
477 lines
17 KiB
Python
"""Tests unitaires pour le module app.fiches.utils.fiche_utils.
|
|
|
|
Ces tests verifient les fonctions utilitaires de gestion des fiches :
|
|
chargement de seuils, migration de metadonnees, rendu Markdown,
|
|
verification de fichiers recents et logique de regeneration.
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from app.fiches.utils.fiche_utils import (
|
|
_migrate_metadata,
|
|
doit_regenerer_fiche,
|
|
fichier_plus_recent,
|
|
load_seuils,
|
|
render_fiche_markdown,
|
|
)
|
|
|
|
# =============================================================================
|
|
# Tests pour load_seuils
|
|
# =============================================================================
|
|
|
|
|
|
class TestLoadSeuils:
|
|
"""Tests pour la fonction load_seuils."""
|
|
|
|
def test_chargement_fichier_valide(self, tmp_path):
|
|
"""Test le chargement d'un fichier YAML valide avec des seuils."""
|
|
contenu = {
|
|
"seuils": {
|
|
"ISG": {"vert": {"max": 40}, "rouge": {"min": 70}},
|
|
}
|
|
}
|
|
fichier = tmp_path / "seuils.yaml"
|
|
fichier.write_text(yaml.dump(contenu), encoding="utf-8")
|
|
|
|
resultat = load_seuils(fichier)
|
|
|
|
assert "ISG" in resultat
|
|
assert resultat["ISG"]["vert"]["max"] == 40
|
|
|
|
def test_fichier_sans_cle_seuils(self, tmp_path):
|
|
"""Test le chargement d'un fichier YAML sans la cle 'seuils'."""
|
|
fichier = tmp_path / "vide.yaml"
|
|
fichier.write_text("autre_cle: valeur", encoding="utf-8")
|
|
|
|
resultat = load_seuils(fichier)
|
|
|
|
assert resultat == {}
|
|
|
|
def test_fichier_inexistant_leve_erreur(self):
|
|
"""Test qu'un fichier inexistant leve une exception."""
|
|
with pytest.raises(FileNotFoundError):
|
|
load_seuils("/chemin/inexistant/seuils.yaml")
|
|
|
|
|
|
# =============================================================================
|
|
# Tests pour _migrate_metadata
|
|
# =============================================================================
|
|
|
|
|
|
class TestMigrateMetadata:
|
|
"""Tests pour la fonction _migrate_metadata."""
|
|
|
|
def test_migration_sheet_type(self):
|
|
"""Test la migration de sheet_type vers type_fiche."""
|
|
meta = {"sheet_type": "minerai", "titre": "Test"}
|
|
resultat = _migrate_metadata(meta)
|
|
|
|
assert "type_fiche" in resultat
|
|
assert resultat["type_fiche"] == "minerai"
|
|
assert "sheet_type" not in resultat
|
|
|
|
def test_migration_indice_code(self):
|
|
"""Test la migration de indice_code vers indice_court."""
|
|
meta = {"indice_code": "IVC"}
|
|
resultat = _migrate_metadata(meta)
|
|
|
|
assert "indice_court" in resultat
|
|
assert resultat["indice_court"] == "IVC"
|
|
assert "indice_code" not in resultat
|
|
|
|
def test_pas_de_migration_si_nouvelle_cle_existe(self):
|
|
"""Test que la migration n'ecrase pas une cle existante."""
|
|
meta = {"sheet_type": "ancien", "type_fiche": "nouveau"}
|
|
resultat = _migrate_metadata(meta)
|
|
|
|
# type_fiche existait deja, sheet_type est conserve
|
|
assert resultat["type_fiche"] == "nouveau"
|
|
assert "sheet_type" in resultat
|
|
|
|
def test_aucune_migration_necessaire(self):
|
|
"""Test avec des metadonnees deja normalisees."""
|
|
meta = {"type_fiche": "composant", "titre": "Test"}
|
|
resultat = _migrate_metadata(meta)
|
|
|
|
assert resultat == {"type_fiche": "composant", "titre": "Test"}
|
|
|
|
def test_dict_vide(self):
|
|
"""Test avec un dictionnaire vide."""
|
|
assert _migrate_metadata({}) == {}
|
|
|
|
|
|
# =============================================================================
|
|
# Tests pour render_fiche_markdown
|
|
# =============================================================================
|
|
|
|
|
|
class TestRenderFicheMarkdown:
|
|
"""Tests pour la fonction render_fiche_markdown."""
|
|
|
|
def test_rendu_basique_avec_placeholders(self, tmp_path):
|
|
"""Test le rendu Markdown avec remplacement de placeholders."""
|
|
licence = tmp_path / "licence.md"
|
|
licence.write_text("---\nLicence CC BY-SA\n---", encoding="utf-8")
|
|
|
|
md_text = """---
|
|
indice: Test
|
|
indice_court: TST
|
|
description: Un test
|
|
---
|
|
## Description
|
|
{{ description }}
|
|
"""
|
|
seuils = {}
|
|
resultat = render_fiche_markdown(md_text, seuils, str(licence))
|
|
|
|
assert "# Test (TST)" in resultat
|
|
assert "Un test" in resultat
|
|
assert "Licence CC BY-SA" in resultat
|
|
|
|
def test_rendu_sans_titre_h1_existant(self, tmp_path):
|
|
"""Test que le titre h1 est ajoute quand absent."""
|
|
licence = tmp_path / "licence.md"
|
|
licence.write_text("", encoding="utf-8")
|
|
|
|
md_text = """---
|
|
indice: Mon Indice
|
|
indice_court: MI
|
|
---
|
|
## Section
|
|
Contenu
|
|
"""
|
|
resultat = render_fiche_markdown(md_text, {}, str(licence))
|
|
|
|
assert resultat.startswith("# Mon Indice (MI)")
|
|
|
|
def test_rendu_avec_titre_h1_existant(self, tmp_path):
|
|
"""Test que le titre h1 n'est pas duplique s'il existe deja."""
|
|
licence = tmp_path / "licence.md"
|
|
licence.write_text("", encoding="utf-8")
|
|
|
|
md_text = """---
|
|
indice: Mon Indice
|
|
indice_court: MI
|
|
---
|
|
# Titre existant
|
|
## Section
|
|
Contenu
|
|
"""
|
|
resultat = render_fiche_markdown(md_text, {}, str(licence))
|
|
|
|
# Le titre existant est conserve, pas de doublon
|
|
assert resultat.count("# Titre existant") == 1
|
|
assert "# Mon Indice (MI)" not in resultat
|
|
|
|
def test_licence_inseree_avant_premier_h2(self, tmp_path):
|
|
"""Test que la licence est inseree avant le premier h2."""
|
|
licence = tmp_path / "licence.md"
|
|
licence.write_text("LICENCE_MARKER", encoding="utf-8")
|
|
|
|
md_text = """---
|
|
indice: Test
|
|
indice_court: T
|
|
---
|
|
Introduction
|
|
|
|
## Section 1
|
|
|
|
Contenu
|
|
"""
|
|
resultat = render_fiche_markdown(md_text, {}, str(licence))
|
|
|
|
pos_licence = resultat.index("LICENCE_MARKER")
|
|
pos_h2 = resultat.index("## Section 1")
|
|
assert pos_licence < pos_h2
|
|
|
|
def test_licence_ajoutee_en_fin_sans_h2(self, tmp_path):
|
|
"""Test que la licence est ajoutee a la fin s'il n'y a pas de h2."""
|
|
licence = tmp_path / "licence.md"
|
|
licence.write_text("LICENCE_FIN", encoding="utf-8")
|
|
|
|
md_text = """---
|
|
indice: Test
|
|
indice_court: T
|
|
---
|
|
Contenu sans aucun titre de niveau 2
|
|
"""
|
|
resultat = render_fiche_markdown(md_text, {}, str(licence))
|
|
|
|
assert resultat.rstrip().endswith("LICENCE_FIN")
|
|
|
|
def test_erreur_lecture_licence(self):
|
|
"""Test que l'erreur de lecture de la licence est geree gracieusement."""
|
|
mock_st = MagicMock()
|
|
with patch.dict("sys.modules", {"streamlit": mock_st}):
|
|
md_text = """---
|
|
indice: Test
|
|
indice_court: T
|
|
---
|
|
## Section
|
|
Contenu
|
|
"""
|
|
# Chemin de licence inexistant
|
|
resultat = render_fiche_markdown(md_text, {}, "/chemin/inexistant/licence.md")
|
|
|
|
# Le rendu doit quand meme fonctionner
|
|
assert "Contenu" in resultat
|
|
# st.error doit avoir ete appele
|
|
mock_st.error.assert_called_once()
|
|
|
|
def test_rendu_avec_seuils_jinja(self, tmp_path):
|
|
"""Test que les seuils sont accessibles dans le template Jinja2."""
|
|
licence = tmp_path / "licence.md"
|
|
licence.write_text("", encoding="utf-8")
|
|
|
|
md_text = """---
|
|
indice: Test
|
|
indice_court: T
|
|
---
|
|
## Seuils
|
|
Vert max: {{ seuils.ISG.vert.max }}
|
|
"""
|
|
seuils = {"ISG": {"vert": {"max": 40}}}
|
|
resultat = render_fiche_markdown(md_text, seuils, str(licence))
|
|
|
|
assert "Vert max: 40" in resultat
|
|
|
|
def test_rendu_titre_fallback_sur_titre(self, tmp_path):
|
|
"""Test le fallback sur la cle 'titre' si 'indice' est absent."""
|
|
licence = tmp_path / "licence.md"
|
|
licence.write_text("", encoding="utf-8")
|
|
|
|
md_text = """---
|
|
titre: Mon Titre
|
|
indice_court: MT
|
|
---
|
|
## Section
|
|
Contenu
|
|
"""
|
|
resultat = render_fiche_markdown(md_text, {}, str(licence))
|
|
|
|
assert "# Mon Titre (MT)" in resultat
|
|
|
|
|
|
# =============================================================================
|
|
# Tests pour fichier_plus_recent
|
|
# =============================================================================
|
|
|
|
|
|
class TestFichierPlusRecent:
|
|
"""Tests pour la fonction fichier_plus_recent."""
|
|
|
|
def test_fichier_plus_recent_que_reference(self, tmp_path):
|
|
"""Test avec un fichier plus recent que la reference."""
|
|
fichier = tmp_path / "recent.txt"
|
|
fichier.write_text("contenu", encoding="utf-8")
|
|
|
|
# Reference bien dans le passe
|
|
reference = datetime(2020, 1, 1, tzinfo=timezone.utc)
|
|
|
|
assert fichier_plus_recent(str(fichier), reference) is True
|
|
|
|
def test_fichier_plus_ancien_que_reference(self, tmp_path):
|
|
"""Test avec un fichier plus ancien que la reference."""
|
|
fichier = tmp_path / "ancien.txt"
|
|
fichier.write_text("contenu", encoding="utf-8")
|
|
|
|
# Reference bien dans le futur
|
|
reference = datetime(2099, 12, 31, tzinfo=timezone.utc)
|
|
|
|
assert fichier_plus_recent(str(fichier), reference) is False
|
|
|
|
def test_fichier_inexistant(self):
|
|
"""Test avec un chemin vers un fichier inexistant."""
|
|
assert fichier_plus_recent("/chemin/inexistant.txt", datetime.now(tz=timezone.utc)) is False
|
|
|
|
def test_chemin_none(self):
|
|
"""Test avec un chemin None."""
|
|
assert fichier_plus_recent(None, datetime.now(tz=timezone.utc)) is False
|
|
|
|
|
|
# =============================================================================
|
|
# Tests pour doit_regenerer_fiche
|
|
# =============================================================================
|
|
|
|
|
|
def _ivc_recent(chemin, _ref):
|
|
"""Retourne True si le chemin correspond au fichier IVC."""
|
|
return chemin == "/chemin/ivc.csv"
|
|
|
|
|
|
def _ics_recent(chemin, _ref):
|
|
"""Retourne True si le chemin correspond au fichier ICS."""
|
|
return chemin == "/chemin/ics.csv"
|
|
|
|
|
|
class TestDoitRegenererFiche:
|
|
"""Tests pour la fonction doit_regenerer_fiche."""
|
|
|
|
@patch("app.fiches.utils.fiche_utils.recuperer_date_dernier_commit")
|
|
def test_fichier_html_inexistant(self, mock_commit):
|
|
"""Test que la regeneration est requise si le fichier HTML n'existe pas."""
|
|
resultat = doit_regenerer_fiche(
|
|
"/chemin/inexistant.html",
|
|
"composant",
|
|
"Processeur",
|
|
"https://gitea.example.com/commits",
|
|
{},
|
|
)
|
|
assert resultat is True
|
|
# recuperer_date_dernier_commit ne doit pas etre appele
|
|
mock_commit.assert_not_called()
|
|
|
|
@patch("app.fiches.utils.fiche_utils.recuperer_date_dernier_commit")
|
|
def test_commit_distant_plus_recent(self, mock_commit, tmp_path):
|
|
"""Test la regeneration quand le commit distant est plus recent."""
|
|
html = tmp_path / "fiche.html"
|
|
html.write_text("<html></html>", encoding="utf-8")
|
|
|
|
# Commit distant dans le futur
|
|
mock_commit.return_value = datetime(2099, 12, 31, tzinfo=timezone.utc)
|
|
|
|
resultat = doit_regenerer_fiche(
|
|
str(html), "composant", "Processeur",
|
|
"https://gitea.example.com/commits", {},
|
|
)
|
|
assert resultat is True
|
|
|
|
@patch("app.fiches.utils.fiche_utils.recuperer_date_dernier_commit")
|
|
def test_commit_distant_none(self, mock_commit, tmp_path):
|
|
"""Test la regeneration quand remote_mtime est None (erreur reseau)."""
|
|
html = tmp_path / "fiche.html"
|
|
html.write_text("<html></html>", encoding="utf-8")
|
|
|
|
mock_commit.return_value = None
|
|
|
|
resultat = doit_regenerer_fiche(
|
|
str(html), "composant", "Processeur",
|
|
"https://gitea.example.com/commits", {},
|
|
)
|
|
assert resultat is True
|
|
|
|
@patch("app.fiches.utils.fiche_utils.fichier_plus_recent")
|
|
@patch("app.fiches.utils.fiche_utils.recuperer_date_dernier_commit")
|
|
def test_ihh_plus_recent(self, mock_commit, mock_recent, tmp_path):
|
|
"""Test la regeneration quand le fichier IHH est plus recent."""
|
|
html = tmp_path / "fiche.html"
|
|
html.write_text("<html></html>", encoding="utf-8")
|
|
|
|
# Commit distant plus ancien
|
|
mock_commit.return_value = datetime(2020, 1, 1, tzinfo=timezone.utc)
|
|
# IHH plus recent
|
|
mock_recent.return_value = True
|
|
|
|
fichiers_criticite = {"IHH": "/chemin/ihh.csv"}
|
|
resultat = doit_regenerer_fiche(
|
|
str(html), "composant", "Processeur",
|
|
"https://gitea.example.com/commits", fichiers_criticite,
|
|
)
|
|
assert resultat is True
|
|
|
|
@patch("app.fiches.utils.fiche_utils.fichier_plus_recent")
|
|
@patch("app.fiches.utils.fiche_utils.recuperer_date_dernier_commit")
|
|
def test_minerai_ivc_plus_recent(self, mock_commit, mock_recent, tmp_path):
|
|
"""Test la regeneration pour un minerai quand IVC est plus recent."""
|
|
html = tmp_path / "fiche.html"
|
|
html.write_text("<html></html>", encoding="utf-8")
|
|
|
|
mock_commit.return_value = datetime(2020, 1, 1, tzinfo=timezone.utc)
|
|
# IHH pas plus recent, mais IVC oui
|
|
mock_recent.side_effect = _ivc_recent
|
|
|
|
fichiers_criticite = {"IHH": "/chemin/ihh.csv", "IVC": "/chemin/ivc.csv"}
|
|
resultat = doit_regenerer_fiche(
|
|
str(html), "minerai", "Lithium",
|
|
"https://gitea.example.com/commits", fichiers_criticite,
|
|
)
|
|
assert resultat is True
|
|
|
|
@patch("app.fiches.utils.fiche_utils.fichier_plus_recent")
|
|
@patch("app.fiches.utils.fiche_utils.recuperer_date_dernier_commit")
|
|
def test_minerai_ics_plus_recent(self, mock_commit, mock_recent, tmp_path):
|
|
"""Test la regeneration pour un minerai quand ICS est plus recent."""
|
|
html = tmp_path / "fiche.html"
|
|
html.write_text("<html></html>", encoding="utf-8")
|
|
|
|
mock_commit.return_value = datetime(2020, 1, 1, tzinfo=timezone.utc)
|
|
# IHH et IVC pas plus recents, mais ICS oui
|
|
mock_recent.side_effect = _ics_recent
|
|
|
|
fichiers_criticite = {
|
|
"IHH": "/chemin/ihh.csv",
|
|
"IVC": "/chemin/ivc.csv",
|
|
"ICS": "/chemin/ics.csv",
|
|
}
|
|
resultat = doit_regenerer_fiche(
|
|
str(html), "minerai", "Cobalt",
|
|
"https://gitea.example.com/commits", fichiers_criticite,
|
|
)
|
|
assert resultat is True
|
|
|
|
@patch("app.fiches.utils.fiche_utils.fichier_plus_recent")
|
|
@patch("app.fiches.utils.fiche_utils.recuperer_date_dernier_commit")
|
|
def test_minerai_dans_nom_fiche(self, mock_commit, mock_recent, tmp_path):
|
|
"""Test la detection de 'minerai' dans le nom de la fiche choisie."""
|
|
html = tmp_path / "fiche.html"
|
|
html.write_text("<html></html>", encoding="utf-8")
|
|
|
|
mock_commit.return_value = datetime(2020, 1, 1, tzinfo=timezone.utc)
|
|
# IHH pas recent, IVC oui
|
|
mock_recent.side_effect = _ivc_recent
|
|
|
|
fichiers_criticite = {"IHH": "/chemin/ihh.csv", "IVC": "/chemin/ivc.csv"}
|
|
# fiche_type n'est pas "minerai" mais le nom contient "minerai"
|
|
resultat = doit_regenerer_fiche(
|
|
str(html), "autre", "Fiche_Minerai_Lithium",
|
|
"https://gitea.example.com/commits", fichiers_criticite,
|
|
)
|
|
assert resultat is True
|
|
|
|
@patch("app.fiches.utils.fiche_utils.fichier_plus_recent")
|
|
@patch("app.fiches.utils.fiche_utils.recuperer_date_dernier_commit")
|
|
def test_aucune_regeneration_necessaire(self, mock_commit, mock_recent, tmp_path):
|
|
"""Test qu'aucune regeneration n'est requise si tout est a jour."""
|
|
html = tmp_path / "fiche.html"
|
|
html.write_text("<html></html>", encoding="utf-8")
|
|
|
|
# Commit distant plus ancien que le fichier HTML
|
|
mock_commit.return_value = datetime(2020, 1, 1, tzinfo=timezone.utc)
|
|
# Aucun fichier de criticite plus recent
|
|
mock_recent.return_value = False
|
|
|
|
fichiers_criticite = {"IHH": "/chemin/ihh.csv"}
|
|
resultat = doit_regenerer_fiche(
|
|
str(html), "composant", "Processeur",
|
|
"https://gitea.example.com/commits", fichiers_criticite,
|
|
)
|
|
assert resultat is False
|
|
|
|
@patch("app.fiches.utils.fiche_utils.fichier_plus_recent")
|
|
@patch("app.fiches.utils.fiche_utils.recuperer_date_dernier_commit")
|
|
def test_composant_ignore_ivc_ics(self, mock_commit, mock_recent, tmp_path):
|
|
"""Test qu'un composant (non minerai) n'utilise pas IVC/ICS."""
|
|
html = tmp_path / "fiche.html"
|
|
html.write_text("<html></html>", encoding="utf-8")
|
|
|
|
mock_commit.return_value = datetime(2020, 1, 1, tzinfo=timezone.utc)
|
|
# IHH pas recent
|
|
mock_recent.return_value = False
|
|
|
|
fichiers_criticite = {
|
|
"IHH": "/chemin/ihh.csv",
|
|
"IVC": "/chemin/ivc.csv",
|
|
"ICS": "/chemin/ics.csv",
|
|
}
|
|
resultat = doit_regenerer_fiche(
|
|
str(html), "composant", "Processeur",
|
|
"https://gitea.example.com/commits", fichiers_criticite,
|
|
)
|
|
assert resultat is False
|
|
# fichier_plus_recent ne doit etre appele qu'une fois (pour IHH)
|
|
assert mock_recent.call_count == 1
|