deep_proxmox.py passe de 1245 à 474 lignes : tout ce qui ne connaît ni « qm » ni pmxcfs vit désormais dans descente.py, prêt pour un second test long. L'extraction a mis à nu ce qui protégeait un hôte qu'on n'a pas créé : rien. a_defaire exigeait « vmid » et « parent_alias », deux clés que seule une descente écrit — la protection tenait parce qu'aucun champ ne décrivait un hôte emprunté. Un champ « cree », écrit à l'instant de la création, la rend explicite et ferme trois portes : la liste de destruction, le repli par NOM de detruire_etage1, et le retrait des entrées ~/.ssh/config de l'utilisateur. Quatrième porte : le dossier des rapports est partagé. « deep_qemu --detruire » aurait pris le rapport le plus récent, fût-il celui d'une descente Proxmox. Le rapport porte son outil ; un rapport plus ancien, qui n'en a pas, est placé par le préfixe de son nom de fichier plutôt que d'être rendu indéfaisable. detruire_etage1 ne devine plus le nom de sa cible : il est obligatoire. Et une sixième étape est née — « cet étage peut-il héberger le suivant ? » — parce que le contrôle du stockage était celui du DÉBUT de l'étage suivant. 64 tests, les cinq garde-fous morts sous mutation. Au passage : la classe LongTestMenuMixin, que mon renommage de répertoire avait rebaptisée long_testMenuMixin sans qu'aucun test le voie. --- EN --- deep_proxmox.py drops from 1245 to 474 lines: everything that knows neither "qm" nor pmxcfs now lives in descente.py, ready for a second long test. The extraction laid bare what protected a host we did not create: nothing. a_defaire required "vmid" and "parent_alias", two keys only a descent writes — the protection held because no field described a borrowed host. A "cree" field, written the instant a machine is created, makes it explicit and closes three doors: the destroy list, detruire_etage1's fallback to the NAME, and the removal of the user's own ~/.ssh/config entries. Fourth door: the report directory is shared. "deep_qemu --detruire" would have taken the most recent report, Proxmox's included. Reports now carry their tool; an older one without it is placed by its filename prefix rather than made undestroyable. detruire_etage1 no longer guesses its target's name: it is mandatory. And a sixth step is born — "can this level host the next?" — because the storage check was the one at the START of the next level. 64 tests, all five guards die under mutation. Along the way: the class LongTestMenuMixin, which my directory rename had turned into long_testMenuMixin without any test noticing. Assisted-by: claude-opus-5 (cherry picked from commit e1bc9ae3cfacd36a502bc88dc0789a4e86ce986b)
98 lines
4 KiB
Python
98 lines
4 KiB
Python
#!/usr/bin/env python3
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
"""Les tests LONGS : de vraies machines, des heures.
|
|
|
|
Ils vivent dans `long_test/` et non dans `test/`, et ce n'est pas un rangement
|
|
de confort : le lanceur unitaire balaie `test/test_*.py` et doit rester
|
|
lançable en quelques secondes, partout. Un test qui crée dix VM n'a rien à y
|
|
faire — il le ferait échouer sur toute machine sans virtualisation, et
|
|
personne ne l'attendrait.
|
|
|
|
Ce menu ne fait que les lancer, en montrant leur sortie en direct : ces
|
|
scripts durent des heures, et une sortie capturée jusqu'à la fin ne dirait
|
|
rien pendant tout ce temps.
|
|
"""
|
|
|
|
import os
|
|
|
|
import click
|
|
|
|
from script.todo.todo_i18n import t
|
|
|
|
# Le répertoire des tests longs, à la racine du dépôt.
|
|
LONGTEST_DIR = "long_test"
|
|
|
|
|
|
class LongTestMenuMixin:
|
|
def _longtest_script(self, nom):
|
|
"""Chemin d'un test long, ou "" s'il n'est pas là."""
|
|
chemin = os.path.join(os.getcwd(), LONGTEST_DIR, nom)
|
|
return chemin if os.path.exists(chemin) else ""
|
|
|
|
def _longtest_run(self, nom, args=""):
|
|
"""Lance un test long, sortie en DIRECT.
|
|
|
|
En direct parce qu'il dure des heures : capturer sa sortie pour
|
|
l'afficher à la fin, c'est ne rien montrer pendant tout ce temps —
|
|
et c'est justement la progression étage par étage qui intéresse.
|
|
"""
|
|
chemin = self._longtest_script(nom)
|
|
if not chemin:
|
|
print(f" ✗ {t('Script not found:')} {LONGTEST_DIR}/{nom}")
|
|
return
|
|
cmd = f"./.venv.erplibre/bin/python {chemin}"
|
|
if args:
|
|
cmd += f" {args}"
|
|
print(f"\n{t('Will execute:')} {cmd}")
|
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
|
|
|
def prompt_execute_longtest(self):
|
|
print(f"⏳ {t('Long tests: real VMs, hours. Not the unit suite.')}")
|
|
choices = [
|
|
{
|
|
"prompt_description": t(
|
|
"Nested Proxmox depth: plan only (dry-run)"
|
|
)
|
|
},
|
|
{"prompt_description": t("Nested Proxmox depth: run it")},
|
|
{"prompt_description": t("Undo what the descent created")},
|
|
]
|
|
help_info = self.fill_help_info(choices)
|
|
while True:
|
|
status = click.prompt(help_info)
|
|
print()
|
|
if status == "0":
|
|
return False
|
|
if status == "1":
|
|
self._longtest_run(
|
|
"deep_proxmox.py",
|
|
f"--depth {self._longtest_depth()} --dry-run",
|
|
)
|
|
elif status == "2":
|
|
# La profondeur est DEMANDÉE : c'est le seul réglage du test,
|
|
# et il décide de sa durée — dix étages, c'est une nuit.
|
|
self._longtest_run(
|
|
"deep_proxmox.py", f"--depth {self._longtest_depth()}"
|
|
)
|
|
elif status == "3":
|
|
# Le script demande « OUI » avant de détruire, mais il liste
|
|
# d'abord : on lui fait faire cette liste À BLANC pour que le
|
|
# choix « 3 » d'une touche ne mène pas directement à un
|
|
# « qm destroy --purge ».
|
|
self._longtest_run("deep_proxmox.py", "--detruire --dry-run")
|
|
if self._is_yes(input(f"\n{t('Destroy all that? (y/N): ')}")):
|
|
self._longtest_run("deep_proxmox.py", "--detruire")
|
|
else:
|
|
print(t("Command not found !"))
|
|
|
|
def _longtest_depth(self):
|
|
"""Profondeur demandée. Trois par défaut, parce que trois marche.
|
|
|
|
Mesuré sur cette machine : les trois premiers étages prennent 280, 495
|
|
et 1 064 secondes — une demi-heure. Le quatrième a demandé 7 h 18
|
|
d'installation et 4 h 20 d'amorçage, et les suivants se comptent en
|
|
jours. Dix par défaut promettait ce qu'aucune machine ne tient.
|
|
"""
|
|
brut = input(f"{t('Depth (default 3): ')}").strip()
|
|
return int(brut) if brut.isdigit() and int(brut) > 0 else 3
|