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
83 lines
2.9 KiB
Python
83 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
"""Une clé de traduction en double écrase la précédente, sans un mot.
|
|
|
|
Python construit un dictionnaire littéral de haut en bas : la dernière
|
|
occurrence gagne. Une clé posée deux fois avec des valeurs différentes fait
|
|
donc qu'un menu affiche la traduction d'un autre, et rien ne le signale — ni
|
|
au chargement, ni à l'exécution, ni au test qui exerce le premier menu.
|
|
|
|
C'est arrivé : « running » et « stopped » existaient déjà, et des clés
|
|
homonymes ajoutées pour un service ont été avalées par elles. Le symptôme
|
|
était un « Service : en cours » là où le fichier disait « actif ».
|
|
|
|
Le contrôle lit le fichier par son ARBRE et non par une expression
|
|
régulière : la mienne n'attrapait que la forme multiligne, et la collision
|
|
était écrite sur une seule ligne.
|
|
"""
|
|
|
|
import ast
|
|
import collections
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
RACINE = Path(__file__).resolve().parent.parent
|
|
I18N = RACINE / "script" / "todo" / "todo_i18n.py"
|
|
sys.path.insert(0, str(RACINE / "script" / "todo"))
|
|
|
|
|
|
def cles_litterales():
|
|
"""Les clés du dictionnaire TRANSLATIONS, dans l'ordre du fichier.
|
|
|
|
Par l'arbre syntaxique : une clé écrite sur une ligne et une clé écrite
|
|
sur plusieurs sont le même nœud, alors qu'elles n'ont pas la même forme
|
|
dans le texte.
|
|
"""
|
|
arbre = ast.parse(I18N.read_text(encoding="utf-8"))
|
|
for noeud in ast.walk(arbre):
|
|
if not isinstance(noeud, ast.Assign):
|
|
continue
|
|
cibles = [c.id for c in noeud.targets if isinstance(c, ast.Name)]
|
|
if "TRANSLATIONS" not in cibles:
|
|
continue
|
|
if not isinstance(noeud.value, ast.Dict):
|
|
continue
|
|
return [
|
|
k.value
|
|
for k in noeud.value.keys
|
|
if isinstance(k, ast.Constant) and isinstance(k.value, str)
|
|
]
|
|
raise AssertionError("TRANSLATIONS introuvable")
|
|
|
|
|
|
class TestClesUniques(unittest.TestCase):
|
|
def test_aucune_cle_nest_ecrite_deux_fois(self):
|
|
compte = collections.Counter(cles_litterales())
|
|
doubles = sorted(k for k, n in compte.items() if n > 1)
|
|
self.assertEqual(
|
|
doubles,
|
|
[],
|
|
"clés en double — la dernière écrase les précédentes, sans un "
|
|
f"mot : {doubles}",
|
|
)
|
|
|
|
def test_le_controle_lit_bien_quelque_chose(self):
|
|
"""Un test qui ne trouve aucune clé passerait toujours."""
|
|
self.assertGreater(len(cles_litterales()), 1000)
|
|
|
|
def test_chaque_cle_porte_les_deux_langues(self):
|
|
import todo_i18n
|
|
|
|
manques = [
|
|
k
|
|
for k, v in todo_i18n.TRANSLATIONS.items()
|
|
if not isinstance(v, dict) or "fr" not in v or "en" not in v
|
|
]
|
|
self.assertEqual(manques, [], f"traductions incomplètes : {manques}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|