[FIX] deploy : la branche du dépôt, et la sortie pendant qu'elle arrive
Trois défauts rapportés sur un déploiement Proxmox. La branche proposée était « dependabot/pip/aiobotocore-3.1.3 » : « git ls-remote » rend la liste par ordre alphabétique, et l'écran en prenait la première. Il propose maintenant la branche du DÉPÔT — celle qu'on a sous les yeux — puis develop, puis master ; la liste met ces trois-là en tête et relègue les branches de robot à la fin. Les deux écrans partagent la règle. La vue de progression n'affichait la sortie qu'à la FIN du travail : « subprocess.run » ne rend rien avant. Sur Proxmox, une VM demande le téléchargement de 325 Mio puis l'import du disque — plusieurs minutes de bloc vide. Elle lit maintenant ligne à ligne. Et « s » y ouvre un ssh sur la VM créée, par son nom, donc par l'entrée ~/.ssh/config qui porte le rebond. --- EN --- Three defects reported on a Proxmox deployment. The branch offered was "dependabot/pip/aiobotocore-3.1.3": "git ls-remote" returns the list in alphabetical order and the screen took its first entry. It now offers the CHECKOUT's branch — the one in front of you — then develop, then master; the list puts those three first and pushes robot branches to the end. Both screens share the rule. The progress view only showed output at the END of a job: "subprocess.run" returns nothing before that. On Proxmox a VM needs a 325 MiB download then a disk import — minutes of an empty block. It now reads line by line. And "s" opens an ssh to the created VM, by name, hence through the ~/.ssh/config entry that carries the jump. Assisted-by: Claude Opus 5
This commit is contained in:
parent
e8ab56a0df
commit
3d8e750af2
9 changed files with 373 additions and 14 deletions
|
|
@ -291,6 +291,53 @@ def plan_rows(vms, domains, extra_disk_gb=0, orphelin=None):
|
|||
return rows
|
||||
|
||||
|
||||
# Les branches qu'on veut voir en premier. « ls-remote » les rend par ordre
|
||||
# alphabétique : la liste commençait donc par « dependabot/pip/aiobotocore »,
|
||||
# et c'est CELLE-LÀ qui était proposée par défaut — rapporté.
|
||||
BRANCHES_EN_TETE = ("develop", "master")
|
||||
|
||||
|
||||
def branch_default(branches, courante=""):
|
||||
"""La branche à proposer : celle du DÉPÔT d'abord.
|
||||
|
||||
Déployer avec la branche qu'on a sous les yeux est le cas courant ; à
|
||||
défaut « develop », puis « master ». Le premier de la liste alphabétique
|
||||
n'a aucune raison d'être un bon défaut, et c'était une branche de
|
||||
dependabot.
|
||||
"""
|
||||
liste = list(branches or ())
|
||||
if not liste:
|
||||
return ""
|
||||
for candidate in (courante,) + BRANCHES_EN_TETE:
|
||||
if candidate and candidate in liste:
|
||||
return candidate
|
||||
return liste[0]
|
||||
|
||||
|
||||
def branch_order(branches, courante=""):
|
||||
"""La liste réordonnée : la branche du dépôt, develop, master, puis le
|
||||
reste, et les branches de robot à la fin.
|
||||
|
||||
Une liste de soixante entrées qui commence par six « dependabot/pip/… »
|
||||
demande de dérouler pour trouver ce qu'on cherche.
|
||||
"""
|
||||
liste = list(branches or ())
|
||||
# Dédoublonnée : la branche du dépôt EST souvent « develop », et la voir
|
||||
# deux fois de suite ferait douter de la liste.
|
||||
tete, vues = [], set()
|
||||
for b in (courante,) + BRANCHES_EN_TETE:
|
||||
if b and b in liste and b not in vues:
|
||||
tete.append(b)
|
||||
vues.add(b)
|
||||
milieu, robots = [], []
|
||||
for b in liste:
|
||||
if b in vues:
|
||||
continue
|
||||
vues.add(b)
|
||||
(robots if b.startswith("dependabot/") else milieu).append(b)
|
||||
return tete + milieu + robots
|
||||
|
||||
|
||||
def gib(nbytes) -> int:
|
||||
"""Octets -> Gio entiers. Le plan compte en Go partout ailleurs : mêler
|
||||
des unités sur la même ligne de totaux la rendrait illisible."""
|
||||
|
|
@ -550,7 +597,10 @@ def run_deploy_progress(jobs, parallelism, run_app: bool = True):
|
|||
|
||||
Un bloc reste DÉPLIÉ tant que la VM tourne, se replie dès qu'elle réussit
|
||||
— et reste ouvert si elle échoue, puisque c'est ce qu'on veut lire."""
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
shlex_quote = shlex.quote
|
||||
import threading
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
|
|
@ -579,6 +629,10 @@ def run_deploy_progress(jobs, parallelism, run_app: bool = True):
|
|||
#hint { height: auto; color: $text-muted; padding: 0 1; }
|
||||
"""
|
||||
BINDINGS = [
|
||||
# « s » comme dans le tableau de bord : une VM qui vient d'être
|
||||
# créée est souvent joignable tout de suite, et on veut y entrer
|
||||
# sans quitter l'écran. Rapporté comme manquant.
|
||||
("s", "ssh", t("SSH")),
|
||||
("c", "copy_current", t("Copy log")),
|
||||
("C", "copy_all", t("Copy all logs")),
|
||||
("q", "quit", t("Quit")),
|
||||
|
|
@ -587,6 +641,10 @@ def run_deploy_progress(jobs, parallelism, run_app: bool = True):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
self._out = {name: "" for _jid, name, _p in jobs}
|
||||
# Ce qui a DÉJÀ été écrit à l'écran, pour ne pas le doubler.
|
||||
self._ecrites = {}
|
||||
# Les VM créées sans erreur : celles où « s » peut entrer.
|
||||
self._reussies = []
|
||||
self._done = 0
|
||||
self._t0 = time.time()
|
||||
self._slots = threading.Semaphore(max(1, parallelism))
|
||||
|
|
@ -609,7 +667,8 @@ def run_deploy_progress(jobs, parallelism, run_app: bool = True):
|
|||
with Vertical():
|
||||
yield Static("", id="summary")
|
||||
yield Static(
|
||||
f" {t('c copy log · C copy all · q quit')}", id="hint"
|
||||
f" {t('s ssh · c copy log · C copy all · q quit')}",
|
||||
id="hint",
|
||||
)
|
||||
yield Button(t("Copy all logs"), id="copyall")
|
||||
yield Footer()
|
||||
|
|
@ -634,27 +693,65 @@ def run_deploy_progress(jobs, parallelism, run_app: bool = True):
|
|||
def _job() -> None:
|
||||
with self._slots:
|
||||
t0 = time.time()
|
||||
lignes = []
|
||||
try:
|
||||
res = subprocess.run(
|
||||
parts, capture_output=True, text=True
|
||||
# « Popen » et LECTURE LIGNE À LIGNE, pas
|
||||
# « subprocess.run » : celui-ci ne rend la sortie
|
||||
# qu'à la fin du travail, et le bloc restait vide
|
||||
# pendant tout le déploiement — rapporté. Sur Proxmox,
|
||||
# une VM demande le téléchargement d'une image de
|
||||
# 325 Mio puis l'import du disque : plusieurs minutes
|
||||
# de silence, et rien à l'écran.
|
||||
proc = subprocess.Popen(
|
||||
parts,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
rc = res.returncode
|
||||
out = (res.stdout or "") + (res.stderr or "")
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
rc, out = 1, str(exc)
|
||||
self.call_from_thread(
|
||||
self._finish,
|
||||
jid,
|
||||
name,
|
||||
1,
|
||||
str(exc),
|
||||
time.time() - t0,
|
||||
)
|
||||
return
|
||||
for ligne in proc.stdout:
|
||||
ligne = ligne.rstrip("\n")
|
||||
lignes.append(ligne)
|
||||
self.call_from_thread(self._ecrire, name, ligne)
|
||||
rc = proc.wait()
|
||||
out = "\n".join(lignes)
|
||||
self.call_from_thread(
|
||||
self._finish, jid, name, rc, out, time.time() - t0
|
||||
)
|
||||
|
||||
self.run_worker(_job, thread=True, group="deploy", exclusive=False)
|
||||
|
||||
def _ecrire(self, name, ligne):
|
||||
"""Une ligne, dès qu'elle arrive. Appelée depuis le fil."""
|
||||
self._ecrites[name] = True
|
||||
try:
|
||||
self.query_one(f"#log_{slug(name)}", RichLog).write(ligne)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _finish(self, jid, name, rc, out, secs):
|
||||
self._out[name] = out
|
||||
results.append((name, rc, out, secs))
|
||||
self._done += 1
|
||||
log = self.query_one(f"#log_{slug(name)}", RichLog)
|
||||
for line in out.strip().splitlines():
|
||||
log.write(line)
|
||||
if rc == 0:
|
||||
self._reussies.append(name)
|
||||
# La sortie est déjà à l'écran, ligne par ligne : la réécrire
|
||||
# la doublerait. Seul un échec avant même le démarrage du
|
||||
# processus n'a rien écrit.
|
||||
if not self._ecrites.get(name):
|
||||
log = self.query_one(f"#log_{slug(name)}", RichLog)
|
||||
for line in out.strip().splitlines():
|
||||
log.write(line)
|
||||
block = self.query_one(f"#{slug(name)}", Collapsible)
|
||||
mark = "✅" if rc == 0 else "❌"
|
||||
block.title = f"{mark} [{jid}] {name} · {fmt_dur(secs)}" + (
|
||||
|
|
@ -665,6 +762,27 @@ def run_deploy_progress(jobs, parallelism, run_app: bool = True):
|
|||
block.collapsed = rc == 0
|
||||
self._refresh_summary()
|
||||
|
||||
def action_ssh(self) -> None:
|
||||
"""Entre dans la VM créée, sans quitter l'écran.
|
||||
|
||||
Par son NOM et non par son adresse : c'est l'entrée
|
||||
~/.ssh/config que le déploiement vient d'écrire qui sait comment
|
||||
l'atteindre — et pour une VM posée sur un hôte Proxmox, elle
|
||||
porte le rebond, seul chemin vers son réseau interne.
|
||||
|
||||
« suspend() » rend le terminal : ssh a besoin du clavier, et
|
||||
Textual le tient encore.
|
||||
"""
|
||||
import os
|
||||
|
||||
if not self._reussies:
|
||||
self.notify(t("No VM created yet."), severity="warning")
|
||||
return
|
||||
# La dernière créée : c'est celle qu'on regarde.
|
||||
nom = self._reussies[-1]
|
||||
with self.suspend():
|
||||
os.system(f"ssh {shlex_quote(nom)} || true")
|
||||
|
||||
# -- presse-papiers (OSC 52 : traverse SSH) --------------------- #
|
||||
def _copy(self, text, what):
|
||||
payload, cut = clip_payload(text)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ from script.todo.deploy_form_lib import (
|
|||
FREE,
|
||||
RES_FIELDS,
|
||||
SELECT_TO_FIELD,
|
||||
branch_default,
|
||||
branch_order,
|
||||
build_vms,
|
||||
disk_note,
|
||||
entry_key,
|
||||
|
|
@ -126,7 +128,12 @@ def run_proxmox_form(ctx, run_app: bool = True):
|
|||
catalog = ctx["catalog"]
|
||||
noms_pris = ctx["names"]
|
||||
vmids_pris = ctx["vmids"]
|
||||
branches = ctx.get("branches") or ["master"]
|
||||
# Ordre et défaut partagés avec le formulaire QEMU/KVM : la liste vient
|
||||
# de « git ls-remote », alphabétique, et commençait donc par une branche
|
||||
# de dependabot — proposée par défaut. Rapporté.
|
||||
branches = branch_order(
|
||||
ctx.get("branches") or ["master"], ctx.get("branch_current")
|
||||
)
|
||||
profiles = ctx.get("install_profiles") or []
|
||||
stockages = ctx.get("storages") or []
|
||||
ponts = ctx.get("bridges") or []
|
||||
|
|
@ -309,7 +316,9 @@ def run_proxmox_form(ctx, run_app: bool = True):
|
|||
)
|
||||
yield Select(
|
||||
[(b, b) for b in branches],
|
||||
value=branches[0],
|
||||
value=branch_default(
|
||||
branches, ctx.get("branch_current")
|
||||
),
|
||||
allow_blank=False,
|
||||
id="f_branch",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -825,6 +825,8 @@ class ProxmoxMenuMixin:
|
|||
"internal_bridge": (pve.INTERNAL_BRIDGE, pve.INTERNAL_CIDR),
|
||||
"build_command": build_command,
|
||||
"branches": self._qemu_branch_list() or ["master"],
|
||||
# La branche du dépôt : c'est elle qu'on déploie le plus souvent.
|
||||
"branch_current": self._qemu_repo_branch(),
|
||||
"install_profiles": self._qemu_install_profiles(),
|
||||
# Même règle qu'en QEMU/KVM : un système peut IMPOSER ce qu'on
|
||||
# installe dessus. Un Proxmox imbriqué recevait sinon ERPLibre et
|
||||
|
|
|
|||
|
|
@ -1254,6 +1254,8 @@ class QemuDeployMixin:
|
|||
"native": native,
|
||||
"domains": self._qemu_list_domains(),
|
||||
"branches": self._qemu_branch_list() or ["master"],
|
||||
# La branche du dépôt : c'est elle qu'on déploie le plus souvent.
|
||||
"branch_current": self._qemu_repo_branch(),
|
||||
"install_profiles": self._qemu_install_profiles(),
|
||||
# Les systèmes qui IMPOSENT ce qu'on installe dessus. Sans cette
|
||||
# table, le formulaire posait ERPLibre + Odoo 18 sur une VM
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ from script.todo.deploy_form_lib import ( # noqa: F401
|
|||
SELECT_TO_FIELD,
|
||||
apply_overrides,
|
||||
apply_profile,
|
||||
branch_default,
|
||||
branch_order,
|
||||
build_spec,
|
||||
build_vms,
|
||||
clean_hostname,
|
||||
|
|
@ -116,7 +118,9 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
no_erplibre = {
|
||||
impose[1].strip() for impose in distro_profiles.values() if impose
|
||||
}
|
||||
branches = ctx.get("branches") or ["master"]
|
||||
branches = branch_order(
|
||||
ctx.get("branches") or ["master"], ctx.get("branch_current")
|
||||
)
|
||||
host_cpu = ctx.get("host_cpu") or 2
|
||||
free_ram = ctx.get("free_ram") or 0
|
||||
free_disk = ctx.get("free_disk") or 0
|
||||
|
|
@ -332,8 +336,8 @@ def run_deploy_form(ctx, run_app: bool = True):
|
|||
)
|
||||
yield Select(
|
||||
[(b, b) for b in branches],
|
||||
value=(
|
||||
"develop" if "develop" in branches else branches[0]
|
||||
value=branch_default(
|
||||
branches, ctx.get("branch_current")
|
||||
),
|
||||
allow_blank=False,
|
||||
id="f_branch",
|
||||
|
|
|
|||
|
|
@ -2304,6 +2304,24 @@ class QemuManageMixin:
|
|||
pass
|
||||
return None, None, arch
|
||||
|
||||
@staticmethod
|
||||
def _qemu_repo_branch():
|
||||
"""Branche du dépôt COURANT, ou '' — le défaut des formulaires.
|
||||
|
||||
On déploie le plus souvent ce qu'on a sous les yeux ; le premier nom
|
||||
de la liste alphabétique, lui, n'a aucune raison d'être bon."""
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return ""
|
||||
nom = (res.stdout or "").strip()
|
||||
return "" if res.returncode or nom == "HEAD" else nom
|
||||
|
||||
def _qemu_branch_list(self):
|
||||
"""Branches distantes d'ERPLibre, triées. Vide si le réseau manque.
|
||||
|
||||
|
|
|
|||
|
|
@ -3986,6 +3986,14 @@ TRANSLATIONS = {
|
|||
"fr": "Fichiers orphelins :",
|
||||
"en": "Orphan files:",
|
||||
},
|
||||
"s ssh · c copy log · C copy all · q quit": {
|
||||
"fr": "s ssh · c copier le log · C copier tout · q quitter",
|
||||
"en": "s ssh · c copy log · C copy all · q quit",
|
||||
},
|
||||
"No VM created yet.": {
|
||||
"fr": "Aucune VM créée pour l'instant.",
|
||||
"en": "No VM created yet.",
|
||||
},
|
||||
# « models » manquait alors que ses trois voisines de la même ligne
|
||||
# étaient traduites : le rapport de qualité de migration sortait
|
||||
# « 42 modules · 812 models · 1 204 vues ».
|
||||
|
|
|
|||
|
|
@ -52,6 +52,58 @@ class TestRessourcesLibres(unittest.TestCase):
|
|||
self.assertEqual(lib.res_value("8", "", 2), 8)
|
||||
|
||||
|
||||
class TestLesBranches(unittest.TestCase):
|
||||
"""« git ls-remote » rend les branches par ordre alphabétique.
|
||||
|
||||
Rapporté : le formulaire Proxmox proposait « dependabot/pip/aiobotocore-
|
||||
3.1.3 » — la première de la liste. Le bon défaut est la branche du DÉPÔT
|
||||
qu'on a sous les yeux, puis develop, puis master.
|
||||
"""
|
||||
|
||||
BRANCHES = [
|
||||
"dependabot/pip/aiobotocore-3.1.3",
|
||||
"dependabot/pip/boto3-1.42.49",
|
||||
"develop",
|
||||
"dev_mobile",
|
||||
"master",
|
||||
]
|
||||
|
||||
def test_the_checkout_branch_wins(self):
|
||||
self.assertEqual(
|
||||
lib.branch_default(self.BRANCHES, "dev_mobile"), "dev_mobile"
|
||||
)
|
||||
|
||||
def test_without_it_develop_then_master(self):
|
||||
self.assertEqual(lib.branch_default(self.BRANCHES, ""), "develop")
|
||||
sans_develop = [b for b in self.BRANCHES if b != "develop"]
|
||||
self.assertEqual(lib.branch_default(sans_develop, ""), "master")
|
||||
|
||||
def test_a_branch_that_no_longer_exists_is_ignored(self):
|
||||
# On déploie une branche qui existe, pas celle qu'on avait localement.
|
||||
self.assertEqual(
|
||||
lib.branch_default(self.BRANCHES, "partie-en-fumée"), "develop"
|
||||
)
|
||||
|
||||
def test_the_last_resort_is_the_first_but_never_by_default(self):
|
||||
self.assertEqual(lib.branch_default(["zzz"], ""), "zzz")
|
||||
self.assertEqual(lib.branch_default([], "develop"), "")
|
||||
|
||||
def test_the_list_puts_the_useful_ones_first(self):
|
||||
ordre = lib.branch_order(self.BRANCHES, "dev_mobile")
|
||||
self.assertEqual(ordre[:3], ["dev_mobile", "develop", "master"])
|
||||
|
||||
def test_the_robots_go_last(self):
|
||||
ordre = lib.branch_order(self.BRANCHES, "develop")
|
||||
self.assertTrue(all(b.startswith("dependabot/") for b in ordre[-2:]))
|
||||
|
||||
def test_nothing_is_lost_nor_duplicated(self):
|
||||
# La branche du dépôt EST souvent « develop » : la voir deux fois
|
||||
# ferait douter de la liste.
|
||||
ordre = lib.branch_order(self.BRANCHES, "develop")
|
||||
self.assertEqual(sorted(ordre), sorted(self.BRANCHES))
|
||||
self.assertEqual(len(ordre), len(set(ordre)))
|
||||
|
||||
|
||||
class TestPlaceDisque(unittest.TestCase):
|
||||
"""La demande du plan ne dit pas si ça rentre : il faut la place à côté."""
|
||||
|
||||
|
|
|
|||
146
test/test_todo_deploy_progress.py
Normal file
146
test/test_todo_deploy_progress.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
"""La vue de progression d'un déploiement : en direct, et on peut y entrer.
|
||||
|
||||
Rapporté sur un déploiement Proxmox : « les logs ne sont pas live, ils sont
|
||||
apparus à la toute fin » et « il manque les boutons comme s pour se connecter
|
||||
en ssh ». Les deux étaient vrais de cette vue — elle exécutait chaque travail
|
||||
avec `subprocess.run`, qui ne rend sa sortie qu'à la fin, et ses touches se
|
||||
limitaient à copier et quitter.
|
||||
|
||||
Une VM sur Proxmox demande le téléchargement d'une image de 325 Mio puis
|
||||
l'import de son disque : plusieurs minutes d'un bloc vide.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.argv = ["todo.py"]
|
||||
from script.todo.deploy_form_lib import run_deploy_progress # noqa: E402
|
||||
|
||||
try:
|
||||
import textual # noqa: F401
|
||||
|
||||
TEXTUAL = True
|
||||
except Exception: # pragma: no cover - dépend de l'environnement
|
||||
TEXTUAL = False
|
||||
|
||||
|
||||
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
||||
class TestLeFlux(unittest.TestCase):
|
||||
def test_the_lines_appear_while_the_job_runs(self):
|
||||
"""Le cœur du rapport : compté PENDANT, pas après."""
|
||||
jobs = [
|
||||
(
|
||||
"1",
|
||||
"vm-flux",
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
"for i in $(seq 1 6); do echo ligne $i; sleep 1; done",
|
||||
],
|
||||
)
|
||||
]
|
||||
vu = []
|
||||
|
||||
async def scenario():
|
||||
from textual.widgets import RichLog
|
||||
|
||||
app = run_deploy_progress(jobs, 1, run_app=False)
|
||||
async with app.run_test(size=(120, 30)) as pilote:
|
||||
await pilote.pause()
|
||||
journal = app.query_one(RichLog)
|
||||
for _ in range(3):
|
||||
await asyncio.sleep(1.5)
|
||||
await pilote.pause()
|
||||
vu.append(len(journal.lines))
|
||||
vu.append(len(app._reussies))
|
||||
|
||||
asyncio.run(scenario())
|
||||
# Au moins une ligne AVANT la fin, et de plus en plus.
|
||||
self.assertGreater(vu[0], 0, "rien à l'écran pendant le travail")
|
||||
self.assertGreater(vu[2], vu[0], "l'affichage n'avance pas")
|
||||
|
||||
def test_the_output_is_not_written_twice(self):
|
||||
# `_finish` réécrivait tout : la sortie apparaîtrait en double.
|
||||
jobs = [("1", "vm-court", ["bash", "-c", "echo une; echo deux"])]
|
||||
vu = {}
|
||||
|
||||
async def scenario():
|
||||
from textual.widgets import RichLog
|
||||
|
||||
app = run_deploy_progress(jobs, 1, run_app=False)
|
||||
async with app.run_test(size=(120, 30)) as pilote:
|
||||
await pilote.pause()
|
||||
await asyncio.sleep(1.5)
|
||||
await pilote.pause()
|
||||
vu["lignes"] = len(app.query_one(RichLog).lines)
|
||||
|
||||
asyncio.run(scenario())
|
||||
self.assertEqual(vu["lignes"], 2)
|
||||
|
||||
def test_a_command_that_cannot_start_still_says_so(self):
|
||||
# Le processus n'a rien écrit : c'est le seul cas où `_finish` doit
|
||||
# poser la sortie lui-même.
|
||||
jobs = [("1", "vm-absente", ["/n/existe/pas/du/tout"])]
|
||||
vu = {}
|
||||
|
||||
async def scenario():
|
||||
from textual.widgets import RichLog
|
||||
|
||||
app = run_deploy_progress(jobs, 1, run_app=False)
|
||||
async with app.run_test(size=(120, 30)) as pilote:
|
||||
await pilote.pause()
|
||||
await asyncio.sleep(1.0)
|
||||
await pilote.pause()
|
||||
vu["lignes"] = len(app.query_one(RichLog).lines)
|
||||
vu["resultats"] = list(app._reussies)
|
||||
|
||||
asyncio.run(scenario())
|
||||
self.assertGreater(vu["lignes"], 0, "l'échec ne dit rien")
|
||||
self.assertEqual(vu["resultats"], [])
|
||||
|
||||
|
||||
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
||||
class TestLaToucheSsh(unittest.TestCase):
|
||||
def test_the_key_is_offered(self):
|
||||
app = run_deploy_progress([("1", "vm", ["true"])], 1, run_app=False)
|
||||
self.assertIn("s", [b[0] for b in type(app).BINDINGS])
|
||||
|
||||
def test_it_targets_the_vm_that_was_created(self):
|
||||
jobs = [("1", "vm-creee", ["bash", "-c", "echo ok"])]
|
||||
vu = {}
|
||||
|
||||
async def scenario():
|
||||
app = run_deploy_progress(jobs, 1, run_app=False)
|
||||
async with app.run_test(size=(120, 30)) as pilote:
|
||||
await pilote.pause()
|
||||
await asyncio.sleep(1.2)
|
||||
await pilote.pause()
|
||||
vu["reussies"] = list(app._reussies)
|
||||
|
||||
asyncio.run(scenario())
|
||||
# Par son NOM : c'est l'entrée ~/.ssh/config qui sait l'atteindre, et
|
||||
# pour une VM Proxmox elle porte le rebond.
|
||||
self.assertEqual(vu["reussies"], ["vm-creee"])
|
||||
|
||||
def test_a_failed_job_is_not_offered(self):
|
||||
jobs = [("1", "vm-ratee", ["false"])]
|
||||
vu = {}
|
||||
|
||||
async def scenario():
|
||||
app = run_deploy_progress(jobs, 1, run_app=False)
|
||||
async with app.run_test(size=(120, 30)) as pilote:
|
||||
await pilote.pause()
|
||||
await asyncio.sleep(1.2)
|
||||
await pilote.pause()
|
||||
vu["reussies"] = list(app._reussies)
|
||||
|
||||
asyncio.run(scenario())
|
||||
self.assertEqual(vu["reussies"], [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Loading…
Reference in a new issue