Set-OPS-Public/scripts/tests/test_nomenclature_ecriture.py

151 lines
6.4 KiB
Python
Raw Permalink Normal View History

GUI : la vue Nomenclature, et deux fautes que mes bancs ne voyaient pas LA VUE. La nomenclature etait le seul registre que le GUI ne savait pas ecrire du tout : ajouter une fonction exigeait d ouvrir le YAML. Elle a sa vue, et son formulaire est GENERE depuis le schema. Deuxieme registre sur six. couverture_gui verifier passe : les 28 champs des plans reels sont editables. Elle n est pas un registre comme les autres : elle decrit la REGLE dont VMID, VLAN, adresse et passerelle se derivent. Chaque fonction montre ce qu elle derive et les VM qui la portent ; l index est montre mais pas editable, parce qu il est alloue par le site ; valider_nomenclature refuse de retirer une fonction encore portee, ou de designer une zone non declaree. DEUX FAUTES, ET POURQUOI MES BANCS NE LES VOYAIENT PAS. Le formulaire des bases, livre la veille, etait casse dans un navigateur. Il lisait data.schema, or il n existe aucun data global : c est une const locale de charger(). ReferenceError a l ouverture, et zone morte dans sauvegarderBases. Je l avais eprouve sous node EN LUI PASSANT data : le banc reproduisait la fonction, pas sa portee. D ou test_rendu_gui.py, qui charge le JS entier dans un DOM simule et dessine les douze vues, avec son controle negatif. Le schema decrivait reservations comme une table de zones ; le fichier reel est un bloc plat. P61 comparait des NOMS aplatis, donc ne voyait rien. Elle compare desormais aussi la FORME. ECRIRE SANS DEPLACER UN COMMENTAIRE. _fusion_chirurgicale remplace le bloc entier des qu une valeur change : quinze entrees compactes devenaient 42 lignes, et le commentaire du poste d exploitation se retrouvait en tete du bloc, ou il affirmait que collab etait le poste d exploitation. Un commentaire deplace n est pas laid, il est faux. _fusion_table edite les tables ligne a ligne ; le diff fait trois lignes. Au passage : sort_keys triait le schema, donc l ordre des cases a l ecran (reserve_max avant reserve_min) ; et _ecrire_index_nomenclature ecrivait encore par write_text, oubliee au passage des ecritures atomiques. LIMITE : deux registres sur six sont generes, et je n ai toujours pas ouvert cette page dans un navigateur. make prouver : CONFORME, 60 OK, 0 echec, 1 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-08 17:08:45 -04:00
#!/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()