erplibre/test/test_qemu_vm_table.py
Mathieu Benoit cfbdd9406e [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 14:40:38 -04:00

184 lines
7 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""Tableau des infos avancées : RAM utilisée, uptime, largeur.
Deux colonnes ont changé de sens. « RAM » disait l'allocation, elle dit
maintenant l'usage — sur un hyperviseur, savoir qu'une VM de 32 Go n'en occupe
que 4,7 décide s'il reste de la place pour la suivante. Et l'uptime est apparu :
libvirt ne l'expose nulle part, mais le processus QEMU du domaine est né avec
lui.
Ce que ces tests gardent : la formule de la RAM, calibrée contre le « free » de
deux VM réelles, et la largeur de la ligne — un tableau qui déborde de 80
colonnes se replie et devient illisible.
"""
import contextlib
import io
import subprocess
import sys
import unittest
from unittest import mock
sys.argv = ["todo.py"]
from script.todo.todo import TODO # noqa: E402
# Sortie réelle de « virsh dommemstat » sur la VM de migration, en KiO. Le
# « free » de l'invité disait alors : total 11955, used 1216.
DOMMEMSTAT = """actual 12582912
swap_in 0
swap_out 0
major_fault 0
minor_fault 0
unused 1197056
available 12242432
usable 11027456
last_update 1787359366
disk_caches 8211456
"""
class TestUptimeFormat(unittest.TestCase):
def test_seconds_then_minutes_then_hours_then_days(self):
self.assertEqual("45s", TODO._fmt_uptime(45))
self.assertEqual("12m", TODO._fmt_uptime(12 * 60 + 30))
self.assertEqual("19h55", TODO._fmt_uptime(19 * 3600 + 55 * 60))
self.assertEqual("10j13h", TODO._fmt_uptime(10 * 86400 + 13 * 3600))
def test_it_never_exceeds_six_characters(self):
"""La colonne fait six caractères : au-delà, le tableau se décale."""
for secs in (0, 59, 60, 3599, 3600, 86399, 86400, 400 * 86400):
self.assertLessEqual(len(TODO._fmt_uptime(secs)), 6, secs)
def test_precision_drops_as_the_duration_grows(self):
"""Personne ne lit les secondes d'un uptime de dix jours."""
self.assertNotIn("s", TODO._fmt_uptime(19 * 3600))
self.assertNotIn("m", TODO._fmt_uptime(10 * 86400))
class TestMemStat(unittest.TestCase):
def _stat(self, out):
with mock.patch(
"subprocess.run",
return_value=subprocess.CompletedProcess([], 0, out, ""),
):
return TODO._qemu_dommemstat("vm")
def test_used_is_available_minus_usable(self):
"""Calibré contre le « free » de deux VM : 1186 contre 1216 Mo lus dans
l'invité, et 4831 contre 4838 sur l'autre. « available - unused »
donnait 10,8 Go pour une VM qui en occupait 1,2 — il compte le cache.
"""
used, total = self._stat(DOMMEMSTAT)
self.assertAlmostEqual(used / 1024, 1186, delta=5)
self.assertAlmostEqual(total / 1024, 11955, delta=5)
def test_a_stopped_vm_gives_zero_without_crashing(self):
used, total = self._stat("")
self.assertEqual((0, 0), (used, total))
def test_it_survives_virsh_failing(self):
with mock.patch("subprocess.run", side_effect=OSError):
self.assertEqual((0, 0), TODO._qemu_dommemstat("vm"))
def test_missing_usable_is_not_taken_for_zero_use(self):
"""Sans « usable », on ne sait pas : mieux vaut ne rien dire que
d'annoncer une VM qui n'utiliserait rien."""
used, total = self._stat("available 12242432\nunused 1197056\n")
self.assertEqual(0, used)
self.assertGreater(total, 0)
def test_it_asks_for_a_collection_period_first(self):
"""Sans période, le ballon ne rafraîchit rien : une VM qui occupait
4,8 Go en annonçait 490 Mo — vécu. « --live » ne touche pas le XML."""
calls = []
def fake_run(cmd, **kw):
calls.append(cmd)
return subprocess.CompletedProcess([], 0, DOMMEMSTAT, "")
with mock.patch("subprocess.run", side_effect=fake_run):
TODO._qemu_dommemstat("vm")
self.assertIn("--period", calls[0])
self.assertIn("--live", calls[0])
class TestDomainUptime(unittest.TestCase):
def test_it_reads_the_age_of_the_qemu_process(self):
outs = [
subprocess.CompletedProcess([], 0, "1137455\n", ""),
subprocess.CompletedProcess([], 0, " 1195\n", ""),
]
with mock.patch("subprocess.run", side_effect=outs):
self.assertEqual(1195, TODO._qemu_domain_uptime("vm"))
def test_the_pattern_ends_with_a_comma(self):
"""« guest=vm, » et non « guest=vm » : sinon « vm » matcherait aussi
« vm-2 », et l'uptime affiché serait celui d'une autre machine."""
seen = {}
def fake_run(cmd, **kw):
seen.setdefault("cmd", cmd)
return subprocess.CompletedProcess([], 0, "", "")
with mock.patch("subprocess.run", side_effect=fake_run):
TODO._qemu_domain_uptime("vm")
self.assertIn("guest=vm,", seen["cmd"])
def test_a_stopped_domain_has_no_uptime(self):
with mock.patch(
"subprocess.run",
return_value=subprocess.CompletedProcess([], 1, "", ""),
):
self.assertIsNone(TODO._qemu_domain_uptime("vm"))
class TestTheTable(unittest.TestCase):
def _render(self, uptime=1195, mem=(1214 * 1024, 11955 * 1024)):
todo = TODO.__new__(TODO)
todo._qemu_list_domains = lambda: ["erplibre-ubuntu-2604-gnome"]
todo._qemu_domstate = lambda n: "running"
todo._qemu_dominfo = staticmethod(lambda n: (8, 12 * 1024 * 1024))
todo._qemu_main_disk = lambda n: "/var/lib/libvirt/images/x.qcow2"
todo._qemu_disk_sizes = staticmethod(
lambda d: (65 * (1 << 30), 62 * (1 << 30))
)
todo._qemu_dommemstat = staticmethod(lambda n: mem)
todo._qemu_domain_uptime = staticmethod(lambda n: uptime)
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
todo._qemu_list_vms_advanced()
return buf.getvalue()
def test_every_line_fits_in_eighty_columns(self):
"""Le tableau est lu dans un terminal : au-delà de 80, il se replie."""
for line in self._render().splitlines():
if line.startswith("Stockage") or not line.strip():
continue
self.assertLessEqual(len(line), 80, line)
def test_ram_shows_use_over_allocation(self):
out = self._render()
self.assertIn("1.2G/12G", out)
def test_the_uptime_column_is_there(self):
out = self._render()
self.assertIn("Uptime", out)
self.assertIn("19m", out)
def test_a_vm_without_stats_shows_a_dash_not_a_zero(self):
"""« 0.0G/12G » ferait croire à une VM au repos ; « -/12G » dit qu'on
ne sait pas."""
out = self._render(uptime=None, mem=(0, 0))
self.assertIn("-/12G", out)
self.assertNotIn("0.0G/12G", out)
def test_the_full_vm_name_survives_when_it_fits(self):
"""C'est le nom qui distingue les machines : le tronquer trop tôt les
rend indiscernables."""
self.assertIn("erplibre-ubuntu-2604-gnome", self._render())
if __name__ == "__main__":
unittest.main()