Set-OPS-Public/scripts/tests/test_verrou_runner.py
Daniel Allaire 34e961fb61 serveur_ops : la liste hors ligne des collections porte les vraies versions
Le requirements.yml genere portait version: \1 (regex_replace dans un bloc
YAML) ; ansible-galaxy refusait, et la reprise de la reconstruction de
Technolibre s'est arretee apres avoir pris roues et collections au site.
Version tiree du nom par decoupage, sans expression reguliere ; rejoue sur
le poste, les trois collections s'installent hors ligne.
test_verrou_runner.py rend desormais le fichier avec Jinja2 et compare.

94 preuves. CHANGELOG (121).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:35:18 -04:00

107 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""`verrou-runner.yml` : ce que le runner installe, une version par bibliotheque, coherent avec
les epingles — et plus aucune resolution contre PyPI ou Galaxy dans les taches.
POURQUOI (2026-10-09). Le runner de Technolibre portait deux versions de six bibliotheques :
`pip download` resolvait contre PyPI a chaque passage, et PyPI avait bouge entre
l'insemination et le montage. Le verrou nomme chaque fichier ; ce test garde qu'il reste un
verrou (une version par bibliotheque), que ses roues vont au Python declare, et que les
epingles ecrites ailleurs disent la meme chose que lui.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import jinja2
import yaml
RACINE = Path(__file__).resolve().parents[2]
ECHECS: list[str] = []
def verifier(cond: bool, msg: str) -> None:
print(("OK " if cond else "ECHEC ") + msg)
if not cond:
ECHECS.append(msg)
def norme(nom: str) -> str:
return re.sub(r"[-_.]+", "_", nom).lower()
def main() -> int:
v = yaml.safe_load((RACINE / "verrou-runner.yml").read_text())
roues, colls = v["python"], v["collections"]
for e in roues + colls:
verifier(e["url"].endswith("/" + e["nom"]) and re.fullmatch(r"[0-9a-f]{64}", str(e["sha256"])) is not None,
f"{e['nom']} : url qui le nomme, sha256 bien formee")
versions: dict[str, set[str]] = {}
for e in roues:
dist, ver = e["nom"].split("-")[0], e["nom"].split("-")[1]
versions.setdefault(norme(dist), set()).add(ver)
doubles = {d: sorted(vs) for d, vs in versions.items() if len(vs) > 1}
verifier(not doubles, f"une seule version par bibliotheque ({len(versions)} bibliotheques){' : ' + str(doubles) if doubles else ''}")
cible = "cp" + str(v["python_cible"]).replace(".", "")
hors = [e["nom"] for e in roues if e["nom"].split("-")[2].startswith("cp") and "abi3" not in e["nom"]
and e["nom"].split("-")[2] != cible]
verifier(not hors, f"les roues compilees vont a Python {v['python_cible']} ({cible}){' : ' + str(hors) if hors else ''}")
epingles = {}
for l in (RACINE / "requirements-python.txt").read_text().splitlines():
m = re.match(r"^\s*([A-Za-z0-9_.-]+)==([^\s#]+)", l)
if m:
epingles[norme(m.group(1))] = m.group(2)
defauts = yaml.safe_load((RACINE / "roles/serveur_ops/defaults/main.yml").read_text())
m = re.fullmatch(r"\s*([A-Za-z0-9_.-]+)==(\S+)\s*", defauts["serveur_ops_ansible"])
verifier(m is not None, f"serveur_ops_ansible est une epingle exacte ({defauts['serveur_ops_ansible']})")
if m:
epingles[norme(m.group(1))] = m.group(2)
for dist, ver in sorted(epingles.items()):
verifier(versions.get(dist) == {ver}, f"{dist}=={ver} epingle, et c'est la version du verrou ({sorted(versions.get(dist, []))})")
req = yaml.safe_load((RACINE / "requirements.yml").read_text())["collections"]
attendus = sorted(f"{c['name'].replace('.', '-')}-{c['version']}.tar.gz" for c in req)
verifier(sorted(e["nom"] for e in colls) == attendus, f"les collections du verrou sont celles de requirements.yml ({attendus})")
taches = [l for l in (RACINE / "roles/serveur_ops/tasks/main.yml").read_text().splitlines()
if not l.lstrip().startswith("#")]
texte = "\n".join(taches)
verifier('"download"' not in texte and "collection download" not in texte,
"plus aucune resolution contre PyPI ou Galaxy dans serveur_ops (pip / collection download)")
# Une copie fichier par fichier ne cree pas son dossier (2026-10-09 : reconstruction de
# Technolibre arretee sur un runner neuf). Chaque dossier vise doit etre assure AVANT.
assures: set[str] = set()
for t in yaml.safe_load((RACINE / "roles/serveur_ops/tasks/main.yml").read_text()):
f = t.get("ansible.builtin.file") or {}
if f.get("state") == "directory":
assures.add(f["path"])
c = t.get("ansible.builtin.copy") or {}
if str(c.get("dest", "")).endswith("/{{ item.nom }}"):
dossier = c["dest"].rsplit("/", 1)[0]
verifier(dossier in assures, f"« {t['name']} » : son dossier est cree avant ({dossier})")
# LE FICHIER RENDU, PAS LE MODELE (2026-10-09). `'\\1'` dans un bloc `|` restait `\1` :
# `ansible-galaxy` refusait « Non integer values in LooseVersion ('\1') », et la
# reconstruction de Technolibre s'est arretee. Rendu ici avec Jinja2 seul, comme le runner.
tache = next(t for t in yaml.safe_load((RACINE / "roles/serveur_ops/tasks/main.yml").read_text())
if t.get("name") == "Ecrire la liste hors ligne des collections du verrou")
try:
rendu = yaml.safe_load(jinja2.Template(tache["ansible.builtin.copy"]["content"]).render(
serveur_ops_verrou={"collections": colls}))["collections"]
obtenues = sorted(f"{c['name']}={c['version']}" for c in rendu)
except Exception as e: # un filtre propre a Ansible, une syntaxe cassee : c'est un echec
obtenues = [f"rendu impossible : {e}"]
voulues = sorted(f"{c['name'].replace('.', '-')}-{c['version']}.tar.gz={c['version']}" for c in req)
verifier(obtenues == voulues, f"la liste hors ligne rendue porte les vraies versions ({obtenues})")
verifier("verrou-runner.yml" in (RACINE / "roles/serveur_artefacts/tasks/main.yml").read_text(),
"le site tient le verrou du runner")
if ECHECS:
print(f"\n{len(ECHECS)} echec(s).")
return 1
print("\nLe verrou nomme chaque fichier du runner, une version par bibliotheque, et le site le tient.")
return 0
if __name__ == "__main__":
sys.exit(main())