« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous le type de VM — juste avant les sections qu'elle commande — et le suivi la quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe. Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux : sans installation mais AVEC un bureau, le magasin d'applications et les outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser. --- EN --- "Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain it. It becomes "Install software in the VM", moves under the VM type — right before the sections it commands — and the dashboard leaves it, since that watches the VM arrive even when nothing is installed. What it makes ineffective now greys out, title included. Three states, not two: with no install but WITH a desktop, the application store and the "before" tools still act; only those living in the repository go dark. Greying the lot would have lied as much as greying nothing. Assisted-by: Claude Opus 5
218 lines
8.2 KiB
Python
218 lines
8.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 case d'installation : son nom, sa place, et ce qu'elle grise.
|
|
|
|
Elle s'appelait « Installer ERPLibre » alors qu'elle commande TOUTE
|
|
installation — l'hyperviseur Proxmox VE compris. Rapporté après coup : une VM
|
|
Proxmox déployée avec la case décochée est restée une Debian nue, et rien ne
|
|
disait que la case l'expliquait.
|
|
|
|
Elle est donc renommée, placée juste sous le type de VM (les sections qu'elle
|
|
commande viennent après), et ce qu'elle rend sans effet se grise. Trois états
|
|
et non deux : sans installation mais avec un bureau, il se pose encore des
|
|
paquets — griser le magasin d'applications mentirait autant que de laisser
|
|
actif ce qui ne fait rien.
|
|
"""
|
|
|
|
import asyncio
|
|
import re
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.argv = ["todo.py"]
|
|
from script.todo.todo import TODO # noqa: E402
|
|
|
|
FORM = (
|
|
Path(__file__).resolve().parent.parent / "script/todo/qemu_deploy_form.py"
|
|
)
|
|
|
|
try:
|
|
import textual # noqa: F401
|
|
|
|
TEXTUAL = True
|
|
except Exception: # pragma: no cover - dépend de l'environnement
|
|
TEXTUAL = False
|
|
|
|
|
|
class TestSaPlace(unittest.TestCase):
|
|
"""Lu dans la source : l'ordre du panneau est une décision, pas un hasard."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.src = FORM.read_text(encoding="utf-8")
|
|
|
|
def _rang(self, motif):
|
|
m = re.search(re.escape(motif), self.src)
|
|
self.assertIsNotNone(m, motif)
|
|
return m.start()
|
|
|
|
def test_the_install_section_sits_under_the_vm_type(self):
|
|
self.assertLess(
|
|
self._rang('with RadioSet(id="f_type")'),
|
|
self._rang('id="t_install"'),
|
|
)
|
|
|
|
def test_and_before_the_sections_it_commands(self):
|
|
# Magasin d'applications, outils : ils dépendent d'elle, donc ils
|
|
# viennent après.
|
|
for apres in ('id="t_store"', 'id="t_tools"'):
|
|
self.assertLess(self._rang('id="t_install"'), self._rang(apres))
|
|
|
|
def test_the_checkbox_no_longer_claims_to_be_about_erplibre(self):
|
|
self.assertIn('t("Install software in the VM")', self.src)
|
|
self.assertNotIn('t("Install ERPLibre")', self.src)
|
|
|
|
def test_the_monitor_left_the_install_section(self):
|
|
# Rangé dedans, il se serait grisé avec elle — et décocher ERPLibre
|
|
# avait déjà fait disparaître le tableau de bord une fois.
|
|
self.assertLess(
|
|
self._rang('id="t_deploy"'), self._rang('id="f_monitor"')
|
|
)
|
|
|
|
|
|
def contexte():
|
|
todo = TODO.__new__(TODO)
|
|
mod = todo._qemu_import_module()
|
|
todo._qemu_list_domains = lambda: []
|
|
todo._qemu_branch_list = lambda: ["develop", "master"]
|
|
return todo._qemu_form_context(mod)
|
|
|
|
|
|
@unittest.skipUnless(TEXTUAL, "Textual absent")
|
|
class TestCeQuElleGrise(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.ctx = contexte()
|
|
|
|
def _etats(self):
|
|
"""Relève l'état des champs dans les trois situations."""
|
|
from script.todo.qemu_deploy_form import run_deploy_form
|
|
|
|
vu = {}
|
|
|
|
async def scenario():
|
|
from textual.widgets import Checkbox, SelectionList
|
|
|
|
app = run_deploy_form(self.ctx, run_app=False)
|
|
async with app.run_test(size=(200, 60)) as pilote:
|
|
await pilote.pause()
|
|
liste = app.query_one(SelectionList)
|
|
liste.select(liste.get_option_at_index(0).value)
|
|
await pilote.pause()
|
|
await pilote.pause()
|
|
|
|
def relever():
|
|
etat = {}
|
|
for cible in (
|
|
"f_branch",
|
|
"f_profile_install",
|
|
"f_python",
|
|
"f_prod",
|
|
"f_store",
|
|
"f_type",
|
|
"f_monitor",
|
|
"v0_branch",
|
|
"v0_prof",
|
|
):
|
|
try:
|
|
etat[cible] = app.query_one(f"#{cible}").disabled
|
|
except Exception:
|
|
etat[cible] = None
|
|
etat["outils"] = {
|
|
w.id[7:]: w.disabled
|
|
for w in app.query(Checkbox)
|
|
if str(w.id or "").startswith("f_tool_")
|
|
}
|
|
etat["titres"] = {
|
|
w.id
|
|
for w in app.query("#fields Static")
|
|
if "off" in w.classes
|
|
}
|
|
return etat
|
|
|
|
vu["installe"] = relever()
|
|
app.query_one("#f_install", Checkbox).value = False
|
|
await pilote.pause()
|
|
await pilote.pause()
|
|
vu["rien"] = relever()
|
|
list(app.query("#f_type RadioButton"))[1].value = True
|
|
await pilote.pause()
|
|
await pilote.pause()
|
|
vu["bureau_seul"] = relever()
|
|
# Retour au SERVEUR avant de recocher : le magasin
|
|
# d'applications a sa propre raison de se griser (aucune VM
|
|
# graphique sur une distribution à snap), et comparer deux
|
|
# états de type différent ne dirait rien de la case.
|
|
list(app.query("#f_type RadioButton"))[0].value = True
|
|
app.query_one("#f_install", Checkbox).value = True
|
|
await pilote.pause()
|
|
await pilote.pause()
|
|
vu["recoche"] = relever()
|
|
|
|
asyncio.run(scenario())
|
|
return vu
|
|
|
|
@classmethod
|
|
def etats(cls):
|
|
if not hasattr(cls, "_vu"):
|
|
cls._vu = cls()._etats()
|
|
return cls._vu
|
|
|
|
def test_the_branch_and_the_profile_only_serve_an_install(self):
|
|
vu = self.etats()
|
|
for champ in ("f_branch", "f_profile_install", "v0_branch", "v0_prof"):
|
|
self.assertFalse(vu["installe"][champ], champ)
|
|
self.assertTrue(vu["rien"][champ], champ)
|
|
# Même avec un bureau : ils ne servent QU'à ERPLibre.
|
|
self.assertTrue(vu["bureau_seul"][champ], champ)
|
|
|
|
def test_a_desktop_only_install_keeps_what_it_really_uses(self):
|
|
# Le magasin d'applications et « production » servent encore : la
|
|
# commande distante pose des paquets et coupe les mises à jour.
|
|
vu = self.etats()
|
|
self.assertTrue(vu["rien"]["f_store"])
|
|
self.assertFalse(vu["bureau_seul"]["f_store"])
|
|
self.assertTrue(vu["rien"]["f_prod"])
|
|
self.assertFalse(vu["bureau_seul"]["f_prod"])
|
|
|
|
def test_the_tools_that_live_in_the_repository_need_the_install(self):
|
|
# « after » = dans le dépôt ERPLibre. Sans installation, ils n'ont
|
|
# rien où s'installer, bureau ou pas — la commande distante les saute.
|
|
vu = self.etats()
|
|
phases = self.ctx["vm_tool_phases"]
|
|
apres = [k for k, v in phases.items() if v == "after"]
|
|
avant = [k for k, v in phases.items() if v != "after"]
|
|
self.assertTrue(apres and avant, phases)
|
|
for k in apres:
|
|
self.assertTrue(vu["bureau_seul"]["outils"][k], k)
|
|
for k in avant:
|
|
self.assertFalse(vu["bureau_seul"]["outils"][k], k)
|
|
for k in phases:
|
|
self.assertTrue(vu["rien"]["outils"][k], k)
|
|
|
|
def test_the_vm_type_and_the_dashboard_are_never_greyed(self):
|
|
# Le type est l'AUTRE moitié de la décision ; le suivi regarde la VM
|
|
# arriver même quand rien ne s'installe.
|
|
vu = self.etats()
|
|
for cas in ("installe", "rien", "bureau_seul"):
|
|
self.assertFalse(vu[cas]["f_type"], cas)
|
|
self.assertFalse(vu[cas]["f_monitor"], cas)
|
|
|
|
def test_a_greyed_section_reads_as_inactive(self):
|
|
vu = self.etats()
|
|
self.assertEqual(vu["installe"]["titres"], set())
|
|
self.assertIn("t_store", vu["rien"]["titres"])
|
|
self.assertIn("t_tools", vu["rien"]["titres"])
|
|
|
|
def test_ticking_it_back_restores_everything(self):
|
|
vu = self.etats()
|
|
for champ, valeur in vu["recoche"].items():
|
|
if champ in ("outils", "titres"):
|
|
continue
|
|
self.assertEqual(valeur, vu["installe"][champ], champ)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|