149 lines
6.6 KiB
Python
149 lines
6.6 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Le registre des runbooks : sa garde attrape-t-elle vraiment le retard ?
|
||
|
|
|
||
|
|
P83 verifie que le registre REEL dit vrai aujourd'hui. Ce n'est pas la meme chose que
|
||
|
|
verifier que la GARDE fonctionne : un `verifier()` qui rendrait toujours une liste vide
|
||
|
|
passerait P83 tous les jours, et le jour ou le registre prendrait du retard, personne ne
|
||
|
|
le saurait. On lui presente donc quatre registres FAUX, un par forme de retard, et on
|
||
|
|
exige qu'elle les refuse.
|
||
|
|
|
||
|
|
La cinquieme verification porte sur l'index : « Le premier jour d'un site » lance
|
||
|
|
`site-deployer-tout` deux fois, et les deux places n'ont pas le meme sens. Chercher une
|
||
|
|
etape par son nom seul confondrait les deux.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
RACINE = Path(__file__).resolve().parents[2]
|
||
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||
|
|
|
||
|
|
import runbooks as rb # noqa: E402
|
||
|
|
|
||
|
|
SOURCE = (RACINE / "docs/runbooks-construction.yml").read_text(encoding="utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
def _registre(texte: str) -> Path:
|
||
|
|
fd, nom = tempfile.mkstemp(suffix=".yml")
|
||
|
|
os.close(fd)
|
||
|
|
p = Path(nom)
|
||
|
|
p.write_text(texte, encoding="utf-8")
|
||
|
|
return p
|
||
|
|
|
||
|
|
|
||
|
|
class TestGardeDesRunbooks(unittest.TestCase):
|
||
|
|
|
||
|
|
def setUp(self):
|
||
|
|
self.temporaires: list[Path] = []
|
||
|
|
|
||
|
|
def tearDown(self):
|
||
|
|
for p in self.temporaires:
|
||
|
|
p.unlink(missing_ok=True)
|
||
|
|
|
||
|
|
def _verifier(self, texte: str) -> list[str]:
|
||
|
|
p = _registre(texte)
|
||
|
|
self.temporaires.append(p)
|
||
|
|
return rb.verifier(RACINE, p)
|
||
|
|
|
||
|
|
def test_le_registre_reel_ne_porte_aucun_ecart(self):
|
||
|
|
"""Le socle : ce que P83 mesure aussi, mais ici sans le harnais complet."""
|
||
|
|
self.assertEqual(rb.verifier(RACINE), [])
|
||
|
|
|
||
|
|
def test_une_cible_que_le_makefile_ne_porte_plus_est_refusee(self):
|
||
|
|
"""Le cas le plus courant : une cible renommee, un registre qui suit avec retard."""
|
||
|
|
faux = SOURCE.replace("- cible: site-creer", "- cible: site-creer-ancien-nom", 1)
|
||
|
|
ecarts = self._verifier(faux)
|
||
|
|
self.assertTrue(any("ne porte pas cette cible" in e for e in ecarts), ecarts)
|
||
|
|
|
||
|
|
def test_une_cible_documentee_oubliee_est_refusee(self):
|
||
|
|
"""L'oubli silencieux : la console cacherait un pouvoir que le moteur possede."""
|
||
|
|
bloc = """ - cible: dns-bascule-devis
|
||
|
|
nature: mesure
|
||
|
|
pourquoi: >-
|
||
|
|
Basculer nos serveurs de noms changerait-il quelque chose ? Le plan face au DNS
|
||
|
|
reellement en service, avant de toucher a la delegation.
|
||
|
|
"""
|
||
|
|
self.assertIn(bloc, SOURCE)
|
||
|
|
ecarts = self._verifier(SOURCE.replace(bloc, "", 1))
|
||
|
|
self.assertTrue(any("nul runbook ne porte" in e for e in ecarts), ecarts)
|
||
|
|
|
||
|
|
def test_une_etape_sans_pourquoi_est_refusee(self):
|
||
|
|
"""Une etape qui ne dit pas pourquoi elle est la se fait sauter par l'exploitant."""
|
||
|
|
bloc = """ - cible: lint
|
||
|
|
nature: mesure
|
||
|
|
pourquoi: "`ansible-lint` sur tout le depot."
|
||
|
|
"""
|
||
|
|
self.assertIn(bloc, SOURCE)
|
||
|
|
faux = SOURCE.replace(bloc, """ - cible: lint
|
||
|
|
nature: mesure
|
||
|
|
""", 1)
|
||
|
|
ecarts = self._verifier(faux)
|
||
|
|
self.assertTrue(any("aucun « pourquoi »" in e for e in ecarts), ecarts)
|
||
|
|
|
||
|
|
def test_une_exemption_sans_objet_est_refusee(self):
|
||
|
|
"""Une exemption qui survit a sa cible devient un mensonge tranquille."""
|
||
|
|
ecarts = self._verifier(SOURCE + '\n cible-qui-n-existe-pas: "motif quelconque"\n')
|
||
|
|
self.assertTrue(any("exemption devenue sans objet" in e for e in ecarts), ecarts)
|
||
|
|
|
||
|
|
def test_une_nature_hors_vocabulaire_est_refusee(self):
|
||
|
|
"""La console ne sait pas peser une nature qu'elle ignore : elle doit bloquer."""
|
||
|
|
faux = SOURCE.replace(""" - cible: lint
|
||
|
|
nature: mesure""", """ - cible: lint
|
||
|
|
nature: peut-etre""", 1)
|
||
|
|
ecarts = self._verifier(faux)
|
||
|
|
self.assertTrue(any("hors" in e and "vocabulaire" in e for e in ecarts), ecarts)
|
||
|
|
|
||
|
|
def test_une_variable_hors_catalogue_est_refusee(self):
|
||
|
|
"""Sans invite declaree, la console afficherait un champ libre sans le nommer."""
|
||
|
|
faux = SOURCE.replace(" variables: [HOTE]",
|
||
|
|
" variables: [MACHIN]", 1)
|
||
|
|
ecarts = self._verifier(faux)
|
||
|
|
self.assertTrue(any("n'est pas au catalogue" in e for e in ecarts), ecarts)
|
||
|
|
|
||
|
|
|
||
|
|
class TestEtapeDeclaree(unittest.TestCase):
|
||
|
|
"""Le point de controle du serveur : une PLACE, pas un nom."""
|
||
|
|
|
||
|
|
def test_une_cible_absente_du_runbook_ne_se_trouve_pas(self):
|
||
|
|
self.assertIsNone(rb.etape_declaree("locataire-naitre", "raser", RACINE))
|
||
|
|
|
||
|
|
def test_l_index_doit_designer_la_meme_cible(self):
|
||
|
|
etapes = next(r for r in rb.assembler(RACINE) if r["id"] == "locataire-naitre")["etapes"]
|
||
|
|
i = next(i for i, e in enumerate(etapes) if e["cible"] == "instance-courante")
|
||
|
|
self.assertIsNotNone(rb.etape_declaree("locataire-naitre", "instance-courante", RACINE, i))
|
||
|
|
# Le meme nom, une autre place : on ne lance rien.
|
||
|
|
autre = 0 if i != 0 else 1
|
||
|
|
self.assertIsNone(rb.etape_declaree("locataire-naitre", "instance-courante", RACINE, autre))
|
||
|
|
|
||
|
|
def test_une_cible_qui_revient_garde_ses_deux_places(self):
|
||
|
|
"""`site-deployer-tout` est joue DEUX fois, et les deux n'ont pas le meme sens."""
|
||
|
|
rbk = next(r for r in rb.assembler(RACINE) if r["id"] == "site-premier-jour")
|
||
|
|
places = [i for i, e in enumerate(rbk["etapes"]) if e["cible"] == "site-deployer-tout"]
|
||
|
|
self.assertEqual(len(places), 2, "le runbook du premier jour doit porter deux passages")
|
||
|
|
for i in places:
|
||
|
|
self.assertIsNotNone(
|
||
|
|
rb.etape_declaree("site-premier-jour", "site-deployer-tout", RACINE, i))
|
||
|
|
|
||
|
|
def test_le_libelle_vient_du_makefile_et_non_du_registre(self):
|
||
|
|
"""La preuve que le registre ne recopie rien : le libelle est celui du Makefile."""
|
||
|
|
aides = rb.cibles_du_makefile(RACINE)
|
||
|
|
for rbk in rb.assembler(RACINE):
|
||
|
|
for etape in rbk["etapes"]:
|
||
|
|
self.assertEqual(etape["libelle"], aides[etape["cible"]])
|
||
|
|
# Et le registre BRUT n'en porte aucune copie : on lit la donnee, pas le texte
|
||
|
|
# (le mot « libelle » apparait dans les commentaires, qui expliquent justement
|
||
|
|
# pourquoi il ne doit pas y etre).
|
||
|
|
brut = rb.charger(RACINE / "docs/runbooks-construction.yml")
|
||
|
|
for rbk in brut["runbooks"]:
|
||
|
|
for etape in rbk["etapes"]:
|
||
|
|
self.assertNotIn("libelle", etape,
|
||
|
|
f"{rbk['id']} → {etape['cible']} recopie un libelle")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main(verbosity=2)
|