151 lines
6.4 KiB
Python
151 lines
6.4 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Ecrire la nomenclature depuis le GUI ne doit rien emporter d'autre.
|
||
|
|
|
||
|
|
CE QUI EST EN JEU. `instance/plan/nomenclature.yml` porte vingt-deux lignes de
|
||
|
|
commentaire, et elles ne sont pas decoratives : elles disent pourquoi `index` EST RECU
|
||
|
|
ET PAS DECIDE, pourquoi le modele des zones est commun a toute la flotte, et laquelle
|
||
|
|
des fonctions est le poste d'exploitation. C'est de la memoire de decision — le genre
|
||
|
|
qu'on ne retrouve pas en relisant le code.
|
||
|
|
|
||
|
|
TROIS DEGATS MESURES, ET LEURS GARDES :
|
||
|
|
|
||
|
|
1. `safe_dump` efface TOUS les commentaires (incident du 2026-08-18 : 94 lignes dans
|
||
|
|
quatre fichiers). Garde : `_fusion_chirurgicale`.
|
||
|
|
2. Remplacer le BLOC d'une table detruit son style compact — quinze entrees d'une
|
||
|
|
ligne devenues quarante-deux. Garde : `_fusion_table`, ligne a ligne.
|
||
|
|
3. Le meme remplacement HISSE les commentaires internes en tete du bloc. Celui du
|
||
|
|
poste d'exploitation coiffait alors `collab` : il n'etait plus laid, il etait FAUX.
|
||
|
|
Garde : le meme, et c'est le controle negatif de ce fichier qui le prouve.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import shutil
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
RACINE = Path(__file__).resolve().parents[2]
|
||
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||
|
|
|
||
|
|
import yaml # noqa: E402
|
||
|
|
|
||
|
|
import inventory_gui as g # noqa: E402
|
||
|
|
|
||
|
|
SOURCE = RACINE / "instance" / "plan" / "nomenclature.yml"
|
||
|
|
|
||
|
|
|
||
|
|
class TestEcritureNomenclature(unittest.TestCase):
|
||
|
|
|
||
|
|
def setUp(self):
|
||
|
|
self.dossier = Path(tempfile.mkdtemp(prefix="nomenclature-"))
|
||
|
|
self.fichier = self.dossier / "nomenclature.yml"
|
||
|
|
shutil.copy(SOURCE, self.fichier)
|
||
|
|
self.avant = self.fichier.read_text(encoding="utf-8")
|
||
|
|
self.plan = yaml.safe_load(self.avant)
|
||
|
|
|
||
|
|
def tearDown(self):
|
||
|
|
shutil.rmtree(self.dossier, ignore_errors=True)
|
||
|
|
|
||
|
|
def _ecrire(self, plan):
|
||
|
|
g.ecrire_nomenclature(self.fichier, plan)
|
||
|
|
return self.fichier.read_text(encoding="utf-8")
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _commentaires(texte):
|
||
|
|
return [l.strip() for l in texte.splitlines() if l.strip().startswith("#")]
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _ligne_apres(texte, motif):
|
||
|
|
"""La premiere ligne NON commentaire qui suit le commentaire cherche."""
|
||
|
|
lignes = texte.splitlines()
|
||
|
|
for i, l in enumerate(lignes):
|
||
|
|
if motif in l:
|
||
|
|
for suite in lignes[i + 1:]:
|
||
|
|
if not suite.strip().startswith("#"):
|
||
|
|
return suite
|
||
|
|
return None
|
||
|
|
|
||
|
|
def test_les_commentaires_survivent(self):
|
||
|
|
plan = yaml.safe_load(self.avant)
|
||
|
|
plan["fonctions"]["essai"] = {"categorie": 6, "service": 9}
|
||
|
|
apres = self._ecrire(plan)
|
||
|
|
self.assertEqual(self._commentaires(self.avant), self._commentaires(apres),
|
||
|
|
"des commentaires ont ete perdus ou ajoutes")
|
||
|
|
|
||
|
|
def test_le_commentaire_reste_sur_sa_ligne(self):
|
||
|
|
"""LE POINT DUR. Un commentaire deplace explique la mauvaise ligne."""
|
||
|
|
repere = "LE POSTE D'EXPLOITATION"
|
||
|
|
self.assertIn("ops:", self._ligne_apres(self.avant, repere) or "",
|
||
|
|
"le fichier source n'a plus la forme que ce test mesure")
|
||
|
|
plan = yaml.safe_load(self.avant)
|
||
|
|
plan["fonctions"]["essai"] = {"categorie": 6, "service": 9}
|
||
|
|
apres = self._ecrire(plan)
|
||
|
|
self.assertIn("ops:", self._ligne_apres(apres, repere) or "",
|
||
|
|
"le commentaire du poste d'exploitation a change de voisin")
|
||
|
|
|
||
|
|
def test_controle_negatif_le_bloc_entier_deplace_le_commentaire(self):
|
||
|
|
"""LE CONTROLE. Sans `_fusion_table`, le commentaire DOIT se detacher.
|
||
|
|
|
||
|
|
Sans lui, le test ci-dessus passerait aussi sur une ecriture qui ne change rien.
|
||
|
|
On rejoue donc la fusion SANS declarer les tables — exactement le code d'avant.
|
||
|
|
"""
|
||
|
|
plan = yaml.safe_load(self.avant)
|
||
|
|
plan["fonctions"]["essai"] = {"categorie": 6, "service": 9}
|
||
|
|
naif = g._fusion_chirurgicale(self.avant, {"fonctions": plan["fonctions"]})
|
||
|
|
self.assertNotIn("ops:", self._ligne_apres(naif, "LE POSTE D'EXPLOITATION") or "",
|
||
|
|
"le controle negatif ne reproduit plus le defaut : "
|
||
|
|
"_fusion_chirurgicale ne deplace plus le commentaire")
|
||
|
|
|
||
|
|
def test_le_style_compact_survit(self):
|
||
|
|
plan = yaml.safe_load(self.avant)
|
||
|
|
plan["fonctions"]["idm"]["service"] = 7
|
||
|
|
apres = self._ecrire(plan)
|
||
|
|
compactes = [l for l in apres.splitlines() if l.strip().startswith(("collab:", "idm:", "ops:"))]
|
||
|
|
self.assertTrue(all("{" in l for l in compactes),
|
||
|
|
f"le style compact a ete perdu : {compactes}")
|
||
|
|
self.assertIn("idm:", "".join(compactes))
|
||
|
|
self.assertIn("service: 7", apres)
|
||
|
|
|
||
|
|
def test_seules_les_lignes_visees_changent(self):
|
||
|
|
plan = yaml.safe_load(self.avant)
|
||
|
|
plan["fonctions"]["idm"]["service"] = 7
|
||
|
|
apres = self._ecrire(plan)
|
||
|
|
a, b = self.avant.splitlines(), apres.splitlines()
|
||
|
|
self.assertEqual(len(a), len(b), "le nombre de lignes a change")
|
||
|
|
differentes = [i for i, (x, y) in enumerate(zip(a, b)) if x != y]
|
||
|
|
self.assertEqual(len(differentes), 1,
|
||
|
|
f"{len(differentes)} ligne(s) modifiees au lieu d'une")
|
||
|
|
|
||
|
|
def test_index_jamais_ecrit_par_cette_vue(self):
|
||
|
|
"""`index` est ALLOUE par le site : cette vue ne doit pas pouvoir le changer."""
|
||
|
|
plan = yaml.safe_load(self.avant)
|
||
|
|
plan["index"] = 99
|
||
|
|
apres = self._ecrire(plan)
|
||
|
|
self.assertEqual(yaml.safe_load(apres)["index"], self.plan["index"],
|
||
|
|
"la vue Nomenclature a ecrit l'index, qui vient du site")
|
||
|
|
|
||
|
|
def test_retirer_une_fonction_portee_est_refuse(self):
|
||
|
|
serveurs = {"idm-01": {"fonction": "idm"}}
|
||
|
|
plan = yaml.safe_load(self.avant)
|
||
|
|
del plan["fonctions"]["idm"]
|
||
|
|
with self.assertRaises(ValueError) as e:
|
||
|
|
g.valider_nomenclature(plan, serveurs)
|
||
|
|
self.assertIn("idm", str(e.exception))
|
||
|
|
|
||
|
|
def test_zone_non_declaree_est_refusee(self):
|
||
|
|
plan = yaml.safe_load(self.avant)
|
||
|
|
plan["fonctions"]["idm"]["categorie"] = 42
|
||
|
|
with self.assertRaises(ValueError) as e:
|
||
|
|
g.valider_nomenclature(plan, {})
|
||
|
|
self.assertIn("42", str(e.exception))
|
||
|
|
|
||
|
|
def test_une_nomenclature_saine_passe(self):
|
||
|
|
"""Controle du validateur : il ne refuse pas tout."""
|
||
|
|
g.valider_nomenclature(yaml.safe_load(self.avant), g.charger_serveurs(g.FICHIER_SERVEURS))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|