Créer une VM de tête pour héberger un hyperviseur qu'on possède déjà coûte cinq minutes ET un étage d'imbrication — donc de la lenteur, puisque c'est elle qu'on mesure. « --hote » part d'un hôte existant ; le menu le propose sans le rechercher, l'hôte Proxmox déjà retenu étant lu par _pve_host(ask=False). Trois conséquences que le code ne tirait pas : - le plan se dimensionne sur la RACINE, lue par ssh. Le dimensionner sur la machine locale quand les étages vivent ailleurs annoncerait des étages qui ne tiennent pas ; - les délais comptent la profondeur ABSOLUE. Un enfant de niveau 1 posé dans une racine déjà au troisième étage est en réalité au quatrième, et héritait de délais quatre fois trop courts — le défaut même que « delai » raconte avoir corrigé ; - la racine n'est pas un étage atteint. L'y compter décalait de un le total et le code de sortie ; elle va dans une clé à part, et jamais « cree ». « sudo » est DÉDUIT de « id -u » et non supposé, et une racine illisible fait renoncer au lieu d'inventer une capacité. Le menu offre les deux piles et défait chacune séparément — elles partagent le dossier des rapports mais chacune ne connaît que les siens. Un test vérifie que toute entrée affichée a son branchement : ils sont couplés par position, sans garde. 80 tests, quatre garde-fous morts sous mutation. --- EN --- Creating a head VM to host a hypervisor you already own costs five minutes AND one level of nesting — that is, slowness, which is the very thing being measured. "--hote" starts from an existing host; the menu offers it without searching, reading the already-chosen Proxmox host via _pve_host(ask=False). Three consequences the code did not draw: - the plan is sized on the ROOT, read over ssh. Sizing it on the local machine while the levels live elsewhere would announce levels that do not fit; - delays count ABSOLUTE depth. A level-1 child placed in a root already at the third level is really at the fourth, and inherited delays four times too short — the very defect "delai" recounts having fixed; - the root is not a level reached. Counting it shifted the total and the exit code by one; it goes in its own key, and never as "cree". "sudo" is DEDUCED from "id -u" rather than assumed, and an unreadable root makes us give up instead of inventing a capacity. The menu offers both stacks and undoes each separately — they share the report directory but each knows only its own. A test checks that every displayed entry has its branch: they are coupled by position, with no guard. 80 tests, four guards die under mutation. Assisted-by: claude-opus-5 (cherry picked from commit c2ab1a968346458925f55fc95619eb4ebd9d3efa)
181 lines
7.2 KiB
Python
181 lines
7.2 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(
|
|
"Nested QEMU depth: plan only (dry-run)"
|
|
)
|
|
},
|
|
{"prompt_description": t("Nested QEMU depth: run it")},
|
|
{"prompt_description": t("Undo what the descent created")},
|
|
]
|
|
# Chaque choix : le script, et s'il faut demander d'où l'on part.
|
|
scripts = {
|
|
"1": ("deep_proxmox.py", True),
|
|
"2": ("deep_proxmox.py", True),
|
|
"3": ("deep_qemu.py", True),
|
|
"4": ("deep_qemu.py", True),
|
|
}
|
|
help_info = self.fill_help_info(choices)
|
|
while True:
|
|
status = click.prompt(help_info)
|
|
print()
|
|
if status == "0":
|
|
return False
|
|
if status in scripts:
|
|
script, demander = scripts[status]
|
|
# La profondeur est DEMANDÉE : c'est le réglage qui décide de
|
|
# la durée — au-delà de trois étages, tout est 15 à 30 fois
|
|
# plus lent, et cinq se comptent en heures.
|
|
args = f"--depth {self._longtest_depth()}"
|
|
if demander:
|
|
args += self._longtest_depart(script)
|
|
if status in ("1", "3"):
|
|
args += " --dry-run"
|
|
self._longtest_run(script, args)
|
|
elif status == "5":
|
|
self._longtest_defaire()
|
|
else:
|
|
print(t("Command not found !"))
|
|
|
|
def _longtest_defaire(self):
|
|
"""Défaire, chaque pile la sienne.
|
|
|
|
Les deux scripts partagent le dossier des rapports mais chacun ne
|
|
connaît que les siens : lancer les deux ne peut pas faire détruire à
|
|
l'un ce que l'autre a créé.
|
|
|
|
Le script demande « OUI » avant de détruire, mais il LISTE d'abord :
|
|
on lui fait faire cette liste à blanc pour qu'un choix d'une touche ne
|
|
mène pas directement à un « qm destroy --purge ».
|
|
"""
|
|
for script in ("deep_proxmox.py", "deep_qemu.py"):
|
|
self._longtest_run(script, "--detruire --dry-run")
|
|
if self._is_yes(input(f"\n{t('Destroy all that? (y/N): ')}")):
|
|
self._longtest_run(script, "--detruire")
|
|
|
|
def _longtest_depart(self, script):
|
|
"""D'où part la descente : une VM neuve, ou un hôte qu'on a déjà.
|
|
|
|
Créer une machine de tête pour héberger un hyperviseur qu'on possède
|
|
déjà coûte cinq minutes ET un étage d'imbrication — donc de la
|
|
lenteur, puisque c'est justement elle qu'on mesure.
|
|
|
|
L'hôte déjà retenu est proposé sans qu'on ait à le rechercher : c'est
|
|
`_pve_host(ask=False)`, qui ne demande rien et ne dit rien s'il n'y en
|
|
a pas.
|
|
"""
|
|
connu = None
|
|
if script == "deep_proxmox.py":
|
|
try:
|
|
connu = self._pve_host(ask=False)
|
|
except Exception: # noqa: BLE001 - une préférence illisible
|
|
connu = None
|
|
print(f"\n{t('Where does the descent start?')}")
|
|
print(f" [1] {t('Create a fresh QEMU VM as level one')} *")
|
|
if connu:
|
|
print(f" [2] {t('Start from:')} {self._pve_label(connu)}")
|
|
print(f" [3] {t('Start from another existing host')}")
|
|
choix = input(t("Choice (1-3, default 1): ")).strip()
|
|
if choix == "2" and connu:
|
|
return self._longtest_args_hote(connu)
|
|
if choix == "3":
|
|
hote = (
|
|
self._pve_pick_host()
|
|
if script == "deep_proxmox.py"
|
|
else self._longtest_hote_manuel()
|
|
)
|
|
if hote:
|
|
return self._longtest_args_hote(hote)
|
|
print(t("Cancelled."))
|
|
return ""
|
|
|
|
@staticmethod
|
|
def _longtest_args_hote(hote):
|
|
"""Les options que le script attend, à partir d'un dict d'hôte."""
|
|
args = f" --hote {hote['target']}"
|
|
if hote.get("jump"):
|
|
args += f" --jump {hote['jump']}"
|
|
return args
|
|
|
|
def _longtest_hote_manuel(self):
|
|
"""Un hôte libvirt de départ, saisi à la main.
|
|
|
|
Pas de sélecteur vérifié comme pour Proxmox : ce qu'on veut ici, c'est
|
|
un hôte qui porte KVM, et c'est le script qui le CONSTATE au premier
|
|
contrôle — /dev/kvm et l'imbrication — plutôt que le menu qui le
|
|
suppose.
|
|
"""
|
|
cible = input(t("Address (user@host, blank = cancel): ")).strip()
|
|
if not cible:
|
|
return None
|
|
return {
|
|
"target": cible,
|
|
"jump": input(t("SSH jump host (blank = none): ")).strip(),
|
|
}
|
|
|
|
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
|