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

81 lines
3.2 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Chaque gabarit de sonde se rend en un script bash SYNTAXIQUEMENT valide.
POURQUOI (2026-09-28). `sonde-edge.sh.j2` redefinit la syntaxe des commentaires Jinja dans
son en-tete (`#jinja2: comment_start_string:'{=#', ...`). Un commentaire ecrit avec la syntaxe
ordinaire est sorti tel quel dans le script : `bash` a refuse la ligne 35, et la sonde a
rendu une erreur de syntaxe sur les trois edges — l'edge, lui, servait tres bien. Rien ne
l'avait vu avant le deploiement.
On rend donc chaque gabarit avec des variables FACTICES (tout ce qui manque vaut vide), en
respectant l'en-tete `#jinja2:` s'il existe, et on demande a `bash -n` si le resultat se lit.
On ne juge pas ce que la sonde FAIT — seulement qu'elle peut s'executer.
"""
from __future__ import annotations
import re
import subprocess
import sys
import tempfile
from pathlib import Path
import jinja2
ROLES = Path(__file__).resolve().parents[2] / "roles"
class Vide(jinja2.ChainableUndefined):
"""Toute variable absente vaut une chaine vide, iterable et indexable."""
def __str__(self) -> str:
return ""
def __iter__(self):
return iter([])
def rendre(texte: str) -> str:
options = {}
premiere, _, reste = texte.partition("\n")
if premiere.startswith("#jinja2:"):
for cle, val in re.findall(r"(\w+):'([^']*)'", premiere):
options[cle] = val
texte = reste
env = jinja2.Environment(undefined=Vide, **options)
env.filters.update({"to_json": lambda v, **k: "[]", "to_nice_json": lambda v, **k: "[]",
"b64decode": lambda v: "", "from_yaml": lambda v: {}, "bool": bool,
"regex_replace": lambda v, *a, **k: str(v), "quote": lambda v: f"'{v}'"})
# Les filtres d'ANSIBLE, absents de Jinja seul : des substituts neutres suffisent, on ne
# juge que la syntaxe du resultat.
for nom in ("flatten", "dict2items", "items2dict", "ternary", "basename", "dirname",
"mandatory", "ipaddr", "combine", "product", "zip"):
liste = nom in ("flatten", "dict2items", "zip", "product")
env.filters.setdefault(nom, lambda v=None, *a, _liste=liste, **k: [] if _liste else (v or ""))
return env.from_string(texte).render(hostvars={}, groups={})
def main() -> int:
echecs = 0
gabarits = sorted(ROLES.glob("*/templates/sonde-*.sh.j2"))
for g in gabarits:
try:
script = rendre(g.read_text(encoding="utf-8"))
except jinja2.TemplateError as e:
print(f"ECHEC {g.relative_to(ROLES)} : ne se rend pas — {e}")
echecs += 1
continue
with tempfile.NamedTemporaryFile("w", suffix=".sh", delete=False) as f:
f.write(script)
r = subprocess.run(["bash", "-n", f.name], capture_output=True, text=True)
Path(f.name).unlink()
if r.returncode != 0:
print(f"ECHEC {g.relative_to(ROLES)} : bash refuse — {r.stderr.strip()[:160]}")
echecs += 1
if echecs:
print(f"\n{echecs} gabarit(s) de sonde qui ne s'executeraient pas.")
return 1
print(f"OK — {len(gabarits)} gabarits de sonde, tous rendus en bash valide.")
return 0
if __name__ == "__main__":
sys.exit(main())