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
146 lines
5.2 KiB
Python
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)
|