152 lines
6.8 KiB
Python
152 lines
6.8 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Sauvegarder depuis le GUI ne doit emporter aucun commentaire du plan.
|
||
|
|
|
||
|
|
CE QUI ETAIT VRAI JUSQU'AU 2026-09-08, mesure sur les fichiers reels de Chezlepro :
|
||
|
|
|
||
|
|
domaines.yml 6 lignes de commentaire -> 3 (PERD 3)
|
||
|
|
applications.yml 27 -> 5 (PERD 22)
|
||
|
|
serveurs.yml 18 -> 3 (PERD 15)
|
||
|
|
bases-donnees.yml 4 -> 4 (intact — il n'en portait pas)
|
||
|
|
|
||
|
|
Quarante lignes, detruites par n'importe quel clic sur « Sauvegarder » dans les vues
|
||
|
|
Serveurs, Applications ou Domaines. Parmi elles, celle qui explique pourquoi `backup-01`
|
||
|
|
a ete retire — « une supervision creuse est pire qu'aucune : elle est verte » — et celle
|
||
|
|
qui dit dans quel ORDRE les deux roles du runner s'appliquent.
|
||
|
|
|
||
|
|
C'etait l'incident du 2026-08-18 (94 lignes perdues dans les fichiers d'intrants), jamais
|
||
|
|
corrige pour les registres du plan : `_fusion_chirurgicale` avait ete ecrite pour les
|
||
|
|
intrants seuls, et les quatre `ecrire_*` etaient restes au `safe_dump`.
|
||
|
|
|
||
|
|
Ce banc mesure les quatre ecrivains sur les VRAIS fichiers du depot, et non sur un
|
||
|
|
gabarit invente : c'est la seule facon de constater ce que l'operateur perdrait.
|
||
|
|
"""
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
# (nom, fichier source, ecrivain, chargeur, clef racine)
|
||
|
|
REGISTRES = [
|
||
|
|
("domaines.yml", g.FICHIER_DOMAINES, g.ecrire_domaines, g.charger_domaines, "domaines_publics"),
|
||
|
|
("bases-donnees.yml", g.FICHIER_BASES, g.ecrire_bases, g.charger_bases_donnees, "bases_donnees"),
|
||
|
|
("applications.yml", g.FICHIER_APPLICATIONS, g.ecrire_applications, g.charger_applications, "applications"),
|
||
|
|
("serveurs.yml", g.FICHIER_SERVEURS, g.ecrire_serveurs, g.charger_serveurs, "serveurs"),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def commentaires(texte: str) -> list[str]:
|
||
|
|
return [l.strip() for l in texte.splitlines() if l.strip().startswith("#")]
|
||
|
|
|
||
|
|
|
||
|
|
class TestEcriturePlan(unittest.TestCase):
|
||
|
|
|
||
|
|
def setUp(self):
|
||
|
|
self.dossier = Path(tempfile.mkdtemp(prefix="plan-"))
|
||
|
|
|
||
|
|
def tearDown(self):
|
||
|
|
shutil.rmtree(self.dossier, ignore_errors=True)
|
||
|
|
|
||
|
|
def _copie(self, nom, source):
|
||
|
|
cible = self.dossier / nom
|
||
|
|
shutil.copy(source, cible)
|
||
|
|
return cible
|
||
|
|
|
||
|
|
def test_aller_retour_a_vide_est_identique(self):
|
||
|
|
"""Ecrire ce qu'on vient de lire ne doit pas changer un octet.
|
||
|
|
|
||
|
|
C'est la propriete forte : sans elle, toute sauvegarde produit du bruit au diff,
|
||
|
|
et le bruit finit par cacher le vrai changement.
|
||
|
|
"""
|
||
|
|
for nom, source, ecrire, charger, _ in REGISTRES:
|
||
|
|
with self.subTest(registre=nom):
|
||
|
|
cible = self._copie(nom, source)
|
||
|
|
avant = cible.read_text(encoding="utf-8")
|
||
|
|
ecrire(cible, charger(source))
|
||
|
|
self.assertEqual(avant, cible.read_text(encoding="utf-8"))
|
||
|
|
|
||
|
|
def test_aucun_commentaire_perdu_apres_modification(self):
|
||
|
|
for nom, source, ecrire, charger, racine in REGISTRES:
|
||
|
|
with self.subTest(registre=nom):
|
||
|
|
cible = self._copie(nom, source)
|
||
|
|
avant = cible.read_text(encoding="utf-8")
|
||
|
|
registre = charger(source)
|
||
|
|
registre[racine]["essai-banc"] = dict(
|
||
|
|
next(iter(registre[racine].values()), {}) or {})
|
||
|
|
ecrire(cible, registre)
|
||
|
|
self.assertEqual(commentaires(avant), commentaires(cible.read_text(encoding="utf-8")))
|
||
|
|
|
||
|
|
def test_une_entite_ajoutee_arrive_bien(self):
|
||
|
|
for nom, source, ecrire, charger, racine in REGISTRES:
|
||
|
|
with self.subTest(registre=nom):
|
||
|
|
cible = self._copie(nom, source)
|
||
|
|
registre = charger(source)
|
||
|
|
modele = dict(next(iter(registre[racine].values()), {}) or {})
|
||
|
|
registre[racine]["essai-banc"] = modele
|
||
|
|
ecrire(cible, registre)
|
||
|
|
relu = yaml.safe_load(cible.read_text(encoding="utf-8"))
|
||
|
|
self.assertIn("essai-banc", relu[racine])
|
||
|
|
self.assertEqual(relu[racine]["essai-banc"], modele)
|
||
|
|
|
||
|
|
def test_une_entite_retiree_disparait(self):
|
||
|
|
for nom, source, ecrire, charger, racine in REGISTRES:
|
||
|
|
with self.subTest(registre=nom):
|
||
|
|
cible = self._copie(nom, source)
|
||
|
|
avant = cible.read_text(encoding="utf-8")
|
||
|
|
registre = charger(source)
|
||
|
|
if len(registre[racine]) < 2:
|
||
|
|
continue
|
||
|
|
victime = sorted(registre[racine])[0]
|
||
|
|
del registre[racine][victime]
|
||
|
|
ecrire(cible, registre)
|
||
|
|
apres = cible.read_text(encoding="utf-8")
|
||
|
|
relu = yaml.safe_load(apres)
|
||
|
|
self.assertNotIn(victime, relu[racine])
|
||
|
|
# Les autres entites restent, et les commentaires aussi : retirer une
|
||
|
|
# entite n'est pas une raison d'effacer ce qui l'entourait.
|
||
|
|
self.assertEqual(sorted(relu[racine]), sorted(registre[racine]))
|
||
|
|
self.assertEqual(commentaires(avant), commentaires(apres))
|
||
|
|
|
||
|
|
def test_controle_negatif_safe_dump_detruit(self):
|
||
|
|
"""LE CONTROLE. L'ancienne ecriture DOIT perdre des commentaires.
|
||
|
|
|
||
|
|
Sans lui, les tests ci-dessus passeraient sur un fichier qui n'en porte aucun —
|
||
|
|
ils ne mesureraient rien. On rejoue le `safe_dump` d'avant sur les memes fichiers
|
||
|
|
et on exige la perte.
|
||
|
|
"""
|
||
|
|
perdants = []
|
||
|
|
for nom, source, _, charger, racine in REGISTRES:
|
||
|
|
avant = source.read_text(encoding="utf-8")
|
||
|
|
naif = "---\n" + yaml.safe_dump({racine: charger(source)[racine]},
|
||
|
|
default_flow_style=False, sort_keys=False,
|
||
|
|
allow_unicode=True)
|
||
|
|
if len(commentaires(naif)) < len(commentaires(avant)):
|
||
|
|
perdants.append(nom)
|
||
|
|
self.assertIn("serveurs.yml", perdants,
|
||
|
|
"le controle negatif ne reproduit plus le defaut : serveurs.yml "
|
||
|
|
"ne porte-t-il plus de commentaire interne ?")
|
||
|
|
self.assertGreaterEqual(len(perdants), 3, f"seuls {perdants} perdent — banc suspect")
|
||
|
|
|
||
|
|
def test_un_fichier_absent_recoit_son_entete(self):
|
||
|
|
"""Rien a preserver : on ecrit l'entete et le contenu."""
|
||
|
|
cible = self.dossier / "neuf.yml"
|
||
|
|
g.ecrire_domaines(cible, {"domaines_publics": {"exemple.ca": {"autorite": "delegue",
|
||
|
|
"edge": "serveur_nginx"}}})
|
||
|
|
texte = cible.read_text(encoding="utf-8")
|
||
|
|
self.assertIn("# Registre des domaines publics", texte)
|
||
|
|
self.assertIn("exemple.ca", yaml.safe_load(texte)["domaines_publics"])
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|