[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
"""Ce que le test long télécharge, et sur quel système.
|
|
|
|
|
|
|
|
|
|
Trois axes désormais : l'essai (cache, contre-épreuve, témoin), la charge
|
|
|
|
|
(un lot de paquets ou l'installation réelle) et la distribution. Chacun a sa
|
|
|
|
|
façon de se tromper en silence.
|
|
|
|
|
|
|
|
|
|
La charge doit être IDENTIQUE d'une VM à l'autre, sans quoi la comparaison ne
|
|
|
|
|
compare rien. Elle doit aussi correspondre à la famille de la distribution :
|
|
|
|
|
un nom de paquet d'une autre famille fait échouer l'installation loin de sa
|
|
|
|
|
cause, après le déploiement d'une machine entière.
|
|
|
|
|
|
|
|
|
|
Et le catalogue des systèmes n'est PAS recopié : il vient du déploiement. Une
|
|
|
|
|
seconde table proposerait un système que le déploiement ne sait pas installer,
|
|
|
|
|
ce que l'on ne découvrirait qu'en le lançant.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import importlib.util
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
RACINE = Path(__file__).resolve().parent.parent
|
|
|
|
|
sys.path.insert(0, str(RACINE))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def charger():
|
|
|
|
|
chemin = RACINE / "long_test" / "qemu_cache.py"
|
|
|
|
|
spec = importlib.util.spec_from_file_location("qemu_cache_long", chemin)
|
|
|
|
|
mod = importlib.util.module_from_spec(spec)
|
|
|
|
|
spec.loader.exec_module(mod)
|
|
|
|
|
return mod
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QC = charger()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLeCatalogueVientDuDeploiement(unittest.TestCase):
|
|
|
|
|
def test_aucun_systeme_inconnu_du_deploiement(self):
|
|
|
|
|
from script.qemu.deploy_qemu import DISTROS
|
|
|
|
|
|
|
|
|
|
for d in QC.systemes_mesurables():
|
|
|
|
|
self.assertIn(
|
|
|
|
|
d, DISTROS, f"« {d} » n'est pas au catalogue du déploiement"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_chaque_systeme_a_une_famille_servie(self):
|
|
|
|
|
"""Proposer un système dont on ne sait pas installer les paquets, c'est
|
|
|
|
|
déployer une VM entière pour échouer à la dernière étape."""
|
|
|
|
|
for d in QC.systemes_mesurables():
|
|
|
|
|
self.assertIn(
|
|
|
|
|
QC.famille_de(d),
|
|
|
|
|
QC.PAQUETS_MINIMUM,
|
|
|
|
|
f"« {d} » n'a pas de lot de paquets",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_proxmox_est_ecarte(self):
|
|
|
|
|
"""Un hyperviseur : on n'y installe ni ERPLibre ni un lot de
|
|
|
|
|
développement, et le déploiement lui impose déjà son profil."""
|
|
|
|
|
self.assertNotIn("proxmox", QC.systemes_mesurables())
|
|
|
|
|
|
|
|
|
|
def test_les_quatre_familles_sont_couvertes(self):
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
set(QC.PAQUETS_MINIMUM), {"pacman", "apt", "dnf", "zypper"}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLaCharge(unittest.TestCase):
|
|
|
|
|
def test_chaque_famille_rend_une_commande(self):
|
|
|
|
|
for d in sorted(QC.systemes_mesurables()):
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
QC.commande_de_charge(d, "minimum"),
|
|
|
|
|
f"aucune charge minimale pour « {d} »",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_un_systeme_hors_catalogue_ne_rend_rien(self):
|
|
|
|
|
"""Rendre une commande vide plutôt qu'une commande fausse : le test
|
|
|
|
|
s'arrête en le disant, au lieu de lancer un shell vide dans la VM."""
|
|
|
|
|
self.assertEqual(QC.commande_de_charge("haiku", "minimum"), "")
|
|
|
|
|
|
|
|
|
|
def test_la_charge_reelle_contient_la_minimale(self):
|
|
|
|
|
"""Elles partagent leur début, ce qui rend leurs mesures comparables
|
|
|
|
|
sur cette portion — et « make » n'existe pas avant que git l'ait
|
|
|
|
|
cloné."""
|
|
|
|
|
for d in sorted(QC.systemes_mesurables()):
|
|
|
|
|
minimum = QC.commande_de_charge(d, "minimum")
|
|
|
|
|
reelle = QC.commande_de_charge(d, "erplibre")
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
reelle.startswith(minimum),
|
|
|
|
|
f"« {d} » : la charge réelle ne part pas de la minimale",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_la_charge_reelle_clone_puis_installe(self):
|
|
|
|
|
cmd = QC.commande_de_charge("arch", "erplibre")
|
|
|
|
|
self.assertIn("git clone", cmd)
|
|
|
|
|
self.assertIn("install_odoo_18", cmd)
|
|
|
|
|
self.assertLess(
|
|
|
|
|
cmd.index("git clone"),
|
|
|
|
|
cmd.index("install_odoo_18"),
|
|
|
|
|
"la cible make est lancée avant que le dépôt existe",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_chaque_charge_a_son_delai(self):
|
|
|
|
|
"""Un délai unique ferait échouer la courte ou laisserait la longue
|
|
|
|
|
pendre : ERPLibre se compte en heures, le lot en minutes."""
|
|
|
|
|
self.assertEqual(set(QC.DELAI_CHARGE), {"minimum", "erplibre"})
|
|
|
|
|
self.assertGreater(
|
|
|
|
|
QC.DELAI_CHARGE["erplibre"], QC.DELAI_CHARGE["minimum"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_la_charge_attend_cloud_init_avant_tout(self):
|
|
|
|
|
"""cloud-init réécrit la liste des dépôts à son premier démarrage.
|
|
|
|
|
|
|
|
|
|
Une mise à jour lancée pendant ce remplacement récupère une partie des
|
|
|
|
|
index et s'arrête là SANS échouer : l'installation qui suit ne trouve
|
|
|
|
|
plus les paquets de « main », et le message accuse le paquet plutôt que
|
|
|
|
|
le moment. sshd répond bien avant que cloud-init ait fini.
|
|
|
|
|
"""
|
|
|
|
|
for d in sorted(QC.systemes_mesurables()):
|
|
|
|
|
cmd = QC.commande_de_charge(d, "minimum")
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
cmd.startswith(QC.ATTENDRE_CLOUD_INIT),
|
|
|
|
|
f"« {d} » touche au gestionnaire de paquets sans attendre",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_lattente_tolere_un_cloud_init_en_erreur(self):
|
|
|
|
|
"""Il sort en erreur pour un module accessoire — un fuseau que
|
|
|
|
|
l'invité ne connaît pas — et ce n'est pas une raison de renoncer."""
|
|
|
|
|
self.assertIn("|| true", QC.ATTENDRE_CLOUD_INIT)
|
|
|
|
|
|
|
|
|
|
def test_lattente_relit_les_variables_du_cache(self):
|
|
|
|
|
"""La session de la charge s'ouvre avant que cloud-init n'écrive les
|
|
|
|
|
variables du cache : « sudo npm » les relit, pas un npm sans sudo."""
|
|
|
|
|
from script.qemu.deploy_qemu import cache_env_reload
|
|
|
|
|
|
|
|
|
|
i = QC.ATTENDRE_CLOUD_INIT.index("status --wait")
|
|
|
|
|
self.assertIn(cache_env_reload(), QC.ATTENDRE_CLOUD_INIT[i:])
|
|
|
|
|
|
[FIX] déploiement qemu : attendre l'étape finale de cloud-init
« cloud-init status --wait » rend la main dès que cloud-init se déclare en
erreur — un module accessoire y suffit — alors que son étape finale écrit
encore l'autorité du cache, les variables et le fichier sudoers. Une session
ouverte dans cette seconde-là vit sans ces variables, PAM ne relisant plus le
fichier, et sudo n'a alors rien à conserver : l'installation lancée par sudo
rejette le certificat du cache. L'attente guette désormais l'unité
cloud-final, et son seul état « activating » : ce oneshot reste ACTIF une fois
fini, si bien qu'« is-active » y ferait attendre la borne entière pour rien.
--- EN ---
« cloud-init status --wait » returns as soon as cloud-init declares itself in
error — an accessory module is enough — while its final stage still writes the
cache authority, the variables and the sudoers file. A session opened in that
second lives without those variables, PAM never rereading the file, and sudo
then has nothing to keep: an install run by sudo rejects the cache
certificate. The wait now watches the cloud-final unit, and only its
« activating » state: this oneshot stays ACTIVE once finished, so « is-active »
would wait the whole bound for nothing.
Assisted-by: Claude Opus 5
2026-09-16 03:00:57 -04:00
|
|
|
def test_lattente_suit_l_unite_et_non_le_seul_statut(self):
|
|
|
|
|
"""« status --wait » rend la main dès que cloud-init se déclare en
|
|
|
|
|
erreur — un module accessoire y suffit — alors que son étape finale
|
|
|
|
|
écrit encore l'autorité du cache et les variables. Une session ouverte
|
|
|
|
|
à cette seconde-là vit sans elles, et « sudo » n'a rien à conserver :
|
|
|
|
|
« sudo npm » rejette alors le certificat du cache."""
|
|
|
|
|
from script.qemu.deploy_qemu import (
|
|
|
|
|
attente_cloud_final,
|
|
|
|
|
cache_env_reload,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
attente = QC.ATTENDRE_CLOUD_INIT
|
|
|
|
|
self.assertIn("cloud-final", attente_cloud_final())
|
|
|
|
|
self.assertIn(attente_cloud_final(), attente)
|
|
|
|
|
self.assertLess(
|
|
|
|
|
attente.index(attente_cloud_final()),
|
|
|
|
|
attente.index(cache_env_reload()),
|
|
|
|
|
"les variables sont relues avant la fin de l'étape qui les écrit",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_lattente_de_l_unite_est_bornee_et_sans_effet_ailleurs(self):
|
|
|
|
|
"""Une VM sans cloud-init ne doit pas payer l'attente, et une unité
|
|
|
|
|
qui ne finit jamais ne doit pas tenir la campagne indéfiniment."""
|
|
|
|
|
from script.qemu.deploy_qemu import attente_cloud_final
|
|
|
|
|
|
|
|
|
|
court = attente_cloud_final(3)
|
|
|
|
|
self.assertIn("[ $n -ge 3 ]", court)
|
|
|
|
|
self.assertIn("2>/dev/null", court)
|
|
|
|
|
|
|
|
|
|
def test_lattente_guette_activating_et_non_is_active(self):
|
|
|
|
|
"""cloud-final est un « oneshot » qui reste ACTIF une fois terminé :
|
|
|
|
|
« is-active » y est vrai pour toujours. Attendre là-dessus paie la
|
|
|
|
|
borne entière sur chaque VM — cinq minutes — sans jamais rien
|
|
|
|
|
détecter. Seul « activating » dit que l'étape écrit encore."""
|
|
|
|
|
from script.qemu.deploy_qemu import attente_cloud_final
|
|
|
|
|
|
|
|
|
|
attente = attente_cloud_final()
|
|
|
|
|
self.assertIn("activating", attente)
|
|
|
|
|
self.assertNotIn("is-active", attente)
|
|
|
|
|
|
|
|
|
|
def test_lattente_de_l_unite_rend_la_main_sur_cette_machine(self):
|
|
|
|
|
"""Jouée dans un vrai shell, hors VM : l'hôte n'a pas d'étape finale
|
|
|
|
|
en cours, et l'attente doit donc rendre la main tout de suite."""
|
|
|
|
|
import subprocess
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
from script.qemu.deploy_qemu import attente_cloud_final
|
|
|
|
|
|
|
|
|
|
debut = time.monotonic()
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["sh", "-c", attente_cloud_final()],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stderr[-200:])
|
|
|
|
|
self.assertLess(
|
|
|
|
|
time.monotonic() - debut,
|
|
|
|
|
5,
|
|
|
|
|
"l'attente tourne alors que rien n'écrit : elle guette un état"
|
|
|
|
|
" qu'un service oneshot garde pour toujours",
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
def test_la_charge_est_du_shell_valide(self):
|
|
|
|
|
"""Une instruction collée sans séparateur casse la commande entière,
|
|
|
|
|
et la VM ne dit alors pas pourquoi elle n'a rien installé."""
|
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
|
|
for d in sorted(QC.systemes_mesurables()):
|
|
|
|
|
for charge in ("minimum", "erplibre"):
|
|
|
|
|
cmd = QC.commande_de_charge(d, charge)
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-n", "-c", cmd], capture_output=True, text=True
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
res.returncode, 0, f"« {d} », {charge} : {res.stderr}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_apt_ne_cache_plus_ses_echecs(self):
|
|
|
|
|
"""« apt-get update » rend ZÉRO même quand un index n'a pas pu être
|
|
|
|
|
récupéré : il n'émet qu'un avertissement, que « -qq » cachait."""
|
|
|
|
|
rafraichir = QC.PAQUETS_MINIMUM["apt"][0]
|
|
|
|
|
self.assertIn("APT::Update::Error-Mode=any", rafraichir)
|
|
|
|
|
self.assertNotIn("-qq", rafraichir)
|
|
|
|
|
|
|
|
|
|
def test_le_plan_a_blanc_montre_la_commande_exacte(self):
|
|
|
|
|
"""Un plan qui montre autre chose que ce qui sera lancé n'est pas un
|
|
|
|
|
plan."""
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
with mock.patch.object(QC, "dire") as dit:
|
|
|
|
|
QC.poser_les_paquets(
|
|
|
|
|
"10.0.0.1",
|
|
|
|
|
None,
|
|
|
|
|
dry_run=True,
|
|
|
|
|
distro="debian",
|
|
|
|
|
charge="erplibre",
|
|
|
|
|
)
|
|
|
|
|
annonce = " ".join(str(a) for c in dit.call_args_list for a in c.args)
|
|
|
|
|
self.assertIn(QC.commande_de_charge("debian", "erplibre"), annonce)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLesOptions(unittest.TestCase):
|
|
|
|
|
def test_la_version_vide_vient_du_catalogue(self):
|
|
|
|
|
from script.qemu.deploy_qemu import DISTROS
|
|
|
|
|
|
|
|
|
|
for d in sorted(QC.systemes_mesurables()):
|
|
|
|
|
defaut = DISTROS[d][1]
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
defaut, f"« {d} » n'a pas de version par défaut au catalogue"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestLeRapportSeClotSurUnEchec(unittest.TestCase):
|
|
|
|
|
"""Une étape qui échoue arrête la boucle : le rapport doit le dire.
|
|
|
|
|
|
|
|
|
|
Sans « fin » ni « verdict », un rapport laissé par une VM qui n'a pas
|
|
|
|
|
installé se lit comme une exécution encore en cours.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def boucler(self, echoue):
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
import tempfile
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
args = argparse.Namespace(
|
|
|
|
|
dry_run=False,
|
|
|
|
|
sans_cache=False,
|
|
|
|
|
hors_ligne=False,
|
|
|
|
|
distro="debian",
|
|
|
|
|
version="12",
|
|
|
|
|
charge="minimum",
|
|
|
|
|
)
|
|
|
|
|
with tempfile.TemporaryDirectory() as rep:
|
|
|
|
|
fichier = str(Path(rep) / "rapport.json")
|
|
|
|
|
rapport = {"_fichier": fichier, "vms": []}
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(QC, "dire"),
|
|
|
|
|
mock.patch.object(QC, "noter_uuid"),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
QC,
|
|
|
|
|
"deployer",
|
|
|
|
|
return_value="" if echoue == "deployer" else "10.0.0.1",
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
QC, "attendre_ssh", return_value=echoue != "attendre_ssh"
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
QC,
|
|
|
|
|
"poser_les_paquets",
|
|
|
|
|
return_value=echoue != "poser_les_paquets",
|
|
|
|
|
),
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
):
|
|
|
|
|
code = QC._boucle(args, rapport, None, "", 0)
|
|
|
|
|
with open(fichier, encoding="utf-8") as fh:
|
|
|
|
|
return code, json.load(fh)
|
|
|
|
|
|
|
|
|
|
def test_chaque_etape_en_echec_ecrit_fin_et_verdict(self):
|
|
|
|
|
for etape, mot in (
|
|
|
|
|
("deployer", "déploiement"),
|
|
|
|
|
("attendre_ssh", "ssh"),
|
|
|
|
|
("poser_les_paquets", "paquets"),
|
|
|
|
|
):
|
|
|
|
|
with self.subTest(etape=etape):
|
|
|
|
|
code, ecrit = self.boucler(etape)
|
|
|
|
|
self.assertEqual(code, 1)
|
|
|
|
|
self.assertEqual(ecrit.get("verdict"), "échec")
|
|
|
|
|
self.assertTrue(ecrit.get("fin"))
|
|
|
|
|
self.assertIn(mot, ecrit.get("etape_en_echec", ""))
|
|
|
|
|
|
|
|
|
|
def test_un_echec_n_est_pas_un_succes(self):
|
|
|
|
|
"""Le verdict « ok » reste réservé à la boucle menée à son terme."""
|
|
|
|
|
code, ecrit = self.boucler("deployer")
|
|
|
|
|
self.assertNotEqual(ecrit.get("verdict"), "ok")
|
|
|
|
|
self.assertEqual(len(ecrit["vms"]), 1)
|
|
|
|
|
|
|
|
|
|
|
[FIX] hors ligne : HEAD servi, attente NTP levée, étape nommée
Trois défauts qui empêchaient une VM hors ligne d'aboutir. zypper vérifie
chaque dépôt par un HEAD, jamais gardé sous une clé portable : le dépôt était
déclaré invalide alors que le magasin tenait son index, et un HEAD reçoit
désormais les en-têtes du corps du GET gardé. Une image qui attend la
synchronisation NTP avant son étape finale ne démarrait jamais son serveur
ssh, faute de serveur de temps joignable ; l'attente est levée sur une VM
déployée hors ligne. Enfin, une troisième VM en échec nomme l'étape qui l'a
arrêtée, là où le résumé d'une série n'affichait qu'un verdict nu.
--- EN ---
Three faults that kept an offline VM from finishing. zypper checks each
repository with a HEAD, never stored under a portable key: the repository was
declared invalid while the store held its index, and a HEAD now gets the
headers of the stored GET body. An image waiting for NTP synchronisation
before its final stage never started its ssh server, no time server being
reachable; that wait is lifted on a VM deployed offline. Last, a failing third
VM names the step that stopped it, where a series summary showed a bare
verdict.
Assisted-by: Claude Opus 5
2026-09-16 03:00:57 -04:00
|
|
|
class TestLaContreEpreuveNommeSonEtape(unittest.TestCase):
|
|
|
|
|
"""Une troisième VM en échec doit nommer son étape dans le rapport, comme
|
|
|
|
|
les deux premières : le résumé d'une série n'a sinon qu'un « échec »."""
|
|
|
|
|
|
|
|
|
|
def boucler(self, echoue):
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
import tempfile
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
args = argparse.Namespace(
|
|
|
|
|
dry_run=False,
|
|
|
|
|
sans_cache=False,
|
|
|
|
|
hors_ligne=True,
|
|
|
|
|
distro="debian",
|
|
|
|
|
version="12",
|
|
|
|
|
charge="minimum",
|
|
|
|
|
)
|
|
|
|
|
# Les deux premières VM réussissent ; la troisième échoue à l'appel
|
|
|
|
|
# nommé, c'est-à-dire au troisième appel de cette fonction.
|
|
|
|
|
appels = {"n": 0}
|
|
|
|
|
|
|
|
|
|
def selon(nom, reussi):
|
|
|
|
|
def f(*a, **k):
|
|
|
|
|
appels["n"] += nom == echoue
|
|
|
|
|
if nom == echoue and appels["n"] == 3:
|
|
|
|
|
return "" if nom == "deployer" else False
|
|
|
|
|
return reussi
|
|
|
|
|
|
|
|
|
|
return f
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as rep:
|
|
|
|
|
fichier = str(Path(rep) / "rapport.json")
|
|
|
|
|
rapport = {"_fichier": fichier, "vms": []}
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(QC, "dire"),
|
|
|
|
|
mock.patch.object(QC, "noter_uuid"),
|
|
|
|
|
mock.patch.object(QC, "eteindre"),
|
|
|
|
|
mock.patch.object(QC, "verdict", return_value=True),
|
|
|
|
|
mock.patch.object(QC, "couper_lamont", return_value=True),
|
|
|
|
|
mock.patch.object(QC, "rebrancher_lamont", create=True),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
QC, "deployer", side_effect=selon("deployer", "10.0.0.1")
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
QC, "attendre_ssh", side_effect=selon("attendre_ssh", True)
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
QC,
|
|
|
|
|
"poser_les_paquets",
|
|
|
|
|
side_effect=selon("poser_les_paquets", True),
|
|
|
|
|
),
|
[FIX] hors ligne : HEAD servi, attente NTP levée, étape nommée
Trois défauts qui empêchaient une VM hors ligne d'aboutir. zypper vérifie
chaque dépôt par un HEAD, jamais gardé sous une clé portable : le dépôt était
déclaré invalide alors que le magasin tenait son index, et un HEAD reçoit
désormais les en-têtes du corps du GET gardé. Une image qui attend la
synchronisation NTP avant son étape finale ne démarrait jamais son serveur
ssh, faute de serveur de temps joignable ; l'attente est levée sur une VM
déployée hors ligne. Enfin, une troisième VM en échec nomme l'étape qui l'a
arrêtée, là où le résumé d'une série n'affichait qu'un verdict nu.
--- EN ---
Three faults that kept an offline VM from finishing. zypper checks each
repository with a HEAD, never stored under a portable key: the repository was
declared invalid while the store held its index, and a HEAD now gets the
headers of the stored GET body. An image waiting for NTP synchronisation
before its final stage never started its ssh server, no time server being
reachable; that wait is lifted on a VM deployed offline. Last, a failing third
VM names the step that stopped it, where a series summary showed a bare
verdict.
Assisted-by: Claude Opus 5
2026-09-16 03:00:57 -04:00
|
|
|
):
|
|
|
|
|
code = QC._boucle(args, rapport, None, "", 0)
|
|
|
|
|
with open(fichier, encoding="utf-8") as fh:
|
|
|
|
|
return code, json.load(fh)
|
|
|
|
|
|
|
|
|
|
def test_chaque_etape_de_la_troisieme_vm_est_nommee(self):
|
|
|
|
|
for etape, mot in (
|
|
|
|
|
("deployer", "-3 : déploiement"),
|
|
|
|
|
("attendre_ssh", "-3 : ssh"),
|
|
|
|
|
("poser_les_paquets", "-3 : paquets"),
|
|
|
|
|
):
|
|
|
|
|
with self.subTest(etape=etape):
|
|
|
|
|
code, ecrit = self.boucler(etape)
|
|
|
|
|
self.assertEqual(code, 1)
|
|
|
|
|
self.assertEqual(ecrit.get("verdict"), "échec")
|
|
|
|
|
self.assertIn(mot, ecrit.get("etape_en_echec", ""))
|
|
|
|
|
|
|
|
|
|
|
2026-09-16 03:00:17 -04:00
|
|
|
class TestLaTroisiemeVmNaitHorsLigne(unittest.TestCase):
|
|
|
|
|
"""Le déploiement coupe l'audit de npm d'une VM hors ligne : le test long
|
|
|
|
|
doit le lui demander, comme le formulaire, sans quoi il mesure une VM que
|
|
|
|
|
personne ne déploierait ainsi."""
|
|
|
|
|
|
|
|
|
|
def commande(self, **kw):
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(QC, "dire") as dit,
|
|
|
|
|
mock.patch.object(QC, "cle_publique", return_value="/tmp/cle.pub"),
|
2026-09-16 03:00:17 -04:00
|
|
|
):
|
|
|
|
|
QC.deployer("vm", None, dry_run=True, **kw)
|
|
|
|
|
return " ".join(str(a) for c in dit.call_args_list for a in c.args)
|
|
|
|
|
|
|
|
|
|
def test_la_vm_hors_ligne_recoit_offline(self):
|
|
|
|
|
self.assertIn("--offline", self.commande(hors_ligne=True))
|
|
|
|
|
|
|
|
|
|
def test_les_vm_en_ligne_n_en_recoivent_pas(self):
|
|
|
|
|
self.assertNotIn("--offline", self.commande())
|
|
|
|
|
|
|
|
|
|
def test_le_temoin_sans_cache_n_en_recoit_pas(self):
|
|
|
|
|
self.assertNotIn(
|
|
|
|
|
"--offline", self.commande(avec_cache=False, hors_ligne=True)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_la_contre_epreuve_le_demande(self):
|
|
|
|
|
import inspect
|
|
|
|
|
|
|
|
|
|
source = inspect.getsource(QC.contre_epreuve)
|
|
|
|
|
self.assertIn("hors_ligne=True", source)
|
|
|
|
|
|
|
|
|
|
|
2026-09-16 03:00:17 -04:00
|
|
|
class TestPlusieursSystemes(unittest.TestCase):
|
|
|
|
|
"""Une série de campagnes, un système après l'autre."""
|
|
|
|
|
|
|
|
|
|
def test_tous_designe_chaque_systeme_mesurable(self):
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
QC.systemes_demandes("tous"), sorted(QC.systemes_mesurables())
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_une_liste_garde_son_ordre_sans_doublon(self):
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
QC.systemes_demandes("fedora, debian,fedora"), ["fedora", "debian"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_un_nom_inconnu_est_refuse_avant_toute_machine(self):
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
QC.systemes_demandes("fedora,haiku")
|
|
|
|
|
|
|
|
|
|
def lancer(self, argv, codes=None):
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
appels = []
|
|
|
|
|
codes = list(codes or [])
|
|
|
|
|
|
|
|
|
|
def campagne(a):
|
|
|
|
|
appels.append((a.distro, a.version))
|
|
|
|
|
return (codes.pop(0) if codes else 0), ""
|
|
|
|
|
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(QC, "une_campagne", side_effect=campagne),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
QC,
|
|
|
|
|
"detruire",
|
|
|
|
|
side_effect=lambda *x, **k: appels.append("détruire"),
|
|
|
|
|
),
|
|
|
|
|
mock.patch("builtins.print"),
|
2026-09-16 03:00:17 -04:00
|
|
|
):
|
|
|
|
|
code = QC.main(argv)
|
|
|
|
|
return code, appels
|
|
|
|
|
|
|
|
|
|
def test_la_serie_defait_les_machines_entre_deux_systemes(self):
|
|
|
|
|
from script.qemu.deploy_qemu import DISTROS
|
|
|
|
|
|
|
|
|
|
code, appels = self.lancer(
|
|
|
|
|
[
|
|
|
|
|
"--distro",
|
|
|
|
|
"fedora,debian",
|
|
|
|
|
"--charge",
|
|
|
|
|
"erplibre",
|
|
|
|
|
"--hors-ligne",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
appels,
|
|
|
|
|
[
|
|
|
|
|
("fedora", DISTROS["fedora"][1]),
|
|
|
|
|
"détruire",
|
|
|
|
|
("debian", DISTROS["debian"][1]),
|
|
|
|
|
"détruire",
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(code, 0)
|
|
|
|
|
|
|
|
|
|
def test_un_echec_n_arrete_pas_la_serie(self):
|
|
|
|
|
code, appels = self.lancer(["--distro", "fedora,debian"], codes=[1, 0])
|
|
|
|
|
self.assertEqual(len([a for a in appels if a != "détruire"]), 2)
|
|
|
|
|
self.assertEqual(code, 1)
|
|
|
|
|
|
|
|
|
|
def test_a_blanc_rien_n_est_defait(self):
|
|
|
|
|
_, appels = self.lancer(["--distro", "fedora,debian", "--dry-run"])
|
|
|
|
|
self.assertNotIn("détruire", appels)
|
|
|
|
|
|
|
|
|
|
def test_la_version_est_refusee_pour_plusieurs_systemes(self):
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
with mock.patch("sys.stderr"), self.assertRaises(SystemExit):
|
|
|
|
|
QC.main(["--distro", "tous", "--version", "12"])
|
|
|
|
|
|
|
|
|
|
def test_un_seul_systeme_suit_le_chemin_d_avant(self):
|
|
|
|
|
from script.qemu.deploy_qemu import DISTROS
|
|
|
|
|
|
|
|
|
|
_, appels = self.lancer(["--distro", "ubuntu"])
|
|
|
|
|
self.assertEqual(appels, [("ubuntu", DISTROS["ubuntu"][1])])
|
|
|
|
|
|
|
|
|
|
|
[FIX] déploiement et mesure : openSUSE, sudo, console, mémoire
Quatre défauts qu'une campagne complète sur les sept systèmes du catalogue a
mis au jour. La famille zypper visait un faisceau de certificats qu'openSUSE
n'écrit pas, et pip s'arrêtait là, en ligne comme hors ligne. sudo remet
l'environnement à zéro : sans env_keep, une installation lancée par sudo
rejette l'autorité du cache là où PAM ne relit pas /etc/environment. Le
journal de console n'était gardé que pour la voie installateur, si bien
qu'une image cloud bloquée avant ssh ne laissait rien à lire. Et une VM de
8 Gio restait allumée après sa mesure, épuisant la mémoire de l'hôte.
--- EN ---
Four faults a full campaign over the catalogue's seven systems brought out.
The zypper family pointed at a certificate bundle openSUSE does not write,
and pip stopped there, online as well as offline. sudo resets the
environment: without env_keep, an install run by sudo rejects the cache
authority where PAM does not reread /etc/environment. The console log was
kept for the installer path only, so a cloud image stuck before ssh left
nothing to read. And an 8 GiB VM stayed up after its measurement, exhausting
the host's memory.
Assisted-by: Claude Opus 5
2026-09-16 03:00:18 -04:00
|
|
|
class TestUneVmMesureeEstEteinte(unittest.TestCase):
|
|
|
|
|
"""Deux VM de 8 Gio allumées ensemble, puis trois, épuisent la mémoire de
|
|
|
|
|
l'hôte en pleine série : chacune s'éteint dès que sa mesure est faite."""
|
|
|
|
|
|
|
|
|
|
def test_chaque_vm_s_eteint_apres_sa_propre_mesure(self):
|
|
|
|
|
import argparse
|
|
|
|
|
import tempfile
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
ordre = []
|
|
|
|
|
args = argparse.Namespace(
|
|
|
|
|
dry_run=False,
|
|
|
|
|
sans_cache=False,
|
|
|
|
|
hors_ligne=False,
|
|
|
|
|
distro="debian",
|
|
|
|
|
version="12",
|
|
|
|
|
charge="erplibre",
|
|
|
|
|
)
|
|
|
|
|
with tempfile.TemporaryDirectory() as rep:
|
|
|
|
|
rapport = {"_fichier": str(Path(rep) / "r.json"), "vms": []}
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(QC, "dire"),
|
|
|
|
|
mock.patch.object(QC, "noter_uuid"),
|
|
|
|
|
mock.patch.object(QC, "deployer", return_value="10.0.0.1"),
|
|
|
|
|
mock.patch.object(QC, "attendre_ssh", return_value=True),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
QC,
|
|
|
|
|
"poser_les_paquets",
|
|
|
|
|
side_effect=lambda *a, **k: (
|
|
|
|
|
ordre.append("paquets") or True
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(
|
|
|
|
|
QC,
|
|
|
|
|
"eteindre",
|
|
|
|
|
side_effect=lambda nom, *a, **k: ordre.append(nom),
|
|
|
|
|
),
|
|
|
|
|
mock.patch.object(QC, "verdict", return_value=True),
|
[FIX] déploiement et mesure : openSUSE, sudo, console, mémoire
Quatre défauts qu'une campagne complète sur les sept systèmes du catalogue a
mis au jour. La famille zypper visait un faisceau de certificats qu'openSUSE
n'écrit pas, et pip s'arrêtait là, en ligne comme hors ligne. sudo remet
l'environnement à zéro : sans env_keep, une installation lancée par sudo
rejette l'autorité du cache là où PAM ne relit pas /etc/environment. Le
journal de console n'était gardé que pour la voie installateur, si bien
qu'une image cloud bloquée avant ssh ne laissait rien à lire. Et une VM de
8 Gio restait allumée après sa mesure, épuisant la mémoire de l'hôte.
--- EN ---
Four faults a full campaign over the catalogue's seven systems brought out.
The zypper family pointed at a certificate bundle openSUSE does not write,
and pip stopped there, online as well as offline. sudo resets the
environment: without env_keep, an install run by sudo rejects the cache
authority where PAM does not reread /etc/environment. The console log was
kept for the installer path only, so a cloud image stuck before ssh left
nothing to read. And an 8 GiB VM stayed up after its measurement, exhausting
the host's memory.
Assisted-by: Claude Opus 5
2026-09-16 03:00:18 -04:00
|
|
|
):
|
|
|
|
|
QC._boucle(args, rapport, None, "", 0)
|
|
|
|
|
base = QC.base_des_noms(args)
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
ordre, ["paquets", f"{base}-1", "paquets", f"{base}-2"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_eteindre_n_efface_rien(self):
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
|
|
|
|
with mock.patch.object(QC, "dire") as dit:
|
|
|
|
|
QC.eteindre("vm-essai", None, dry_run=True)
|
|
|
|
|
annonce = " ".join(str(a) for c in dit.call_args_list for a in c.args)
|
|
|
|
|
self.assertIn("destroy vm-essai", annonce)
|
|
|
|
|
self.assertNotIn("undefine", annonce)
|
|
|
|
|
|
|
|
|
|
def test_a_blanc_rien_n_est_lance(self):
|
|
|
|
|
from unittest import mock
|
|
|
|
|
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
with (
|
|
|
|
|
mock.patch.object(QC, "dire"),
|
|
|
|
|
mock.patch.object(QC, "executer") as ex,
|
|
|
|
|
):
|
[FIX] déploiement et mesure : openSUSE, sudo, console, mémoire
Quatre défauts qu'une campagne complète sur les sept systèmes du catalogue a
mis au jour. La famille zypper visait un faisceau de certificats qu'openSUSE
n'écrit pas, et pip s'arrêtait là, en ligne comme hors ligne. sudo remet
l'environnement à zéro : sans env_keep, une installation lancée par sudo
rejette l'autorité du cache là où PAM ne relit pas /etc/environment. Le
journal de console n'était gardé que pour la voie installateur, si bien
qu'une image cloud bloquée avant ssh ne laissait rien à lire. Et une VM de
8 Gio restait allumée après sa mesure, épuisant la mémoire de l'hôte.
--- EN ---
Four faults a full campaign over the catalogue's seven systems brought out.
The zypper family pointed at a certificate bundle openSUSE does not write,
and pip stopped there, online as well as offline. sudo resets the
environment: without env_keep, an install run by sudo rejects the cache
authority where PAM does not reread /etc/environment. The console log was
kept for the installer path only, so a cloud image stuck before ssh left
nothing to read. And an 8 GiB VM stayed up after its measurement, exhausting
the host's memory.
Assisted-by: Claude Opus 5
2026-09-16 03:00:18 -04:00
|
|
|
QC.eteindre("vm-essai", None, dry_run=True)
|
|
|
|
|
ex.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|