#!/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()