Set-OPS-Public/scripts/tests/test_nomenclature_ecriture.py
Daniel Allaire 88033f37b8
Some checks are pending
verifier / verifier (push) Waiting to run
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

150 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()