erplibre/test/test_todo_deploy_progress.py
Mathieu Benoit 3d8e750af2 [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
2026-08-25 03:19:18 -04:00

146 lines
5.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)
"""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)