[ADD] script todo : installer les outils manquants, les quatre familles
La réduction sûre listait les outils manquants et s'arrêtait là. Trois
écritures séparées savaient installer, chacune un sous-ensemble
différent : openSUSE posait virt-viewer, mais ni navigateur CLI ni
lm-sensors.
Le binaire n'est presque jamais le paquet : sgdisk vit dans « gdisk »
chez Debian et Fedora, dans « gptfdisk » chez Arch et openSUSE. Deux
règles passent aussi dans la composante : l'ID de la distribution décide
avant le PATH, et la commande s'affiche avant la question.
Vérifié : commandes identiques sur les trois familles déjà couvertes,
4139 tests verts.
--- EN ---
The safe shrink listed the missing tools and stopped there. Three
separate writings knew how to install, each covering a different
subset: openSUSE could put virt-viewer down, but neither a CLI browser
nor lm-sensors.
A binary is almost never the package: sgdisk lives in "gdisk" on Debian
and Fedora, in "gptfdisk" on Arch and openSUSE. Two rules move into the
component as well: the distribution ID decides before the PATH, and the
command is shown before the question.
Checked: commands identical on the three families already covered, and
4139 tests green.
Assisted-by: Claude Opus 5
2026-08-31 07:18:13 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Les paquets qui fournissent les outils de la réduction sûre.
|
|
|
|
|
|
|
|
|
|
La réduction d'un disque de VM refuse de partir sans huit binaires. Le nom du
|
|
|
|
|
binaire n'est presque jamais celui du paquet, et il change de famille en
|
|
|
|
|
famille : sgdisk vit dans « gdisk » chez Debian et Fedora, dans « gptfdisk »
|
|
|
|
|
chez Arch et openSUSE. Une table pareille se démode sans bruit — un trou n'y
|
|
|
|
|
fait rien planter, il fait juste proposer une installation qui n'installe pas
|
|
|
|
|
ce qui manque.
|
|
|
|
|
|
|
|
|
|
Ce qui se vérifie ici sans VM et sans toucher au système : que chaque outil a
|
|
|
|
|
un paquet dans les quatre familles, que la commande construite est celle du
|
|
|
|
|
gestionnaire présent, que les paquets ne sont pas demandés deux fois, et qu'un
|
|
|
|
|
refus comme un échec laissent l'appelant renoncer plutôt que continuer sans
|
|
|
|
|
ses outils.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import io
|
2026-08-31 07:51:19 -04:00
|
|
|
import os
|
[ADD] script todo : installer les outils manquants, les quatre familles
La réduction sûre listait les outils manquants et s'arrêtait là. Trois
écritures séparées savaient installer, chacune un sous-ensemble
différent : openSUSE posait virt-viewer, mais ni navigateur CLI ni
lm-sensors.
Le binaire n'est presque jamais le paquet : sgdisk vit dans « gdisk »
chez Debian et Fedora, dans « gptfdisk » chez Arch et openSUSE. Deux
règles passent aussi dans la composante : l'ID de la distribution décide
avant le PATH, et la commande s'affiche avant la question.
Vérifié : commandes identiques sur les trois familles déjà couvertes,
4139 tests verts.
--- EN ---
The safe shrink listed the missing tools and stopped there. Three
separate writings knew how to install, each covering a different
subset: openSUSE could put virt-viewer down, but neither a CLI browser
nor lm-sensors.
A binary is almost never the package: sgdisk lives in "gdisk" on Debian
and Fedora, in "gptfdisk" on Arch and openSUSE. Two rules move into the
component as well: the distribution ID decides before the PATH, and the
command is shown before the question.
Checked: commands identical on the three families already covered, and
4139 tests green.
Assisted-by: Claude Opus 5
2026-08-31 07:18:13 -04:00
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
from contextlib import redirect_stdout
|
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo import todo_install # noqa: E402
|
[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
|
|
|
from script.todo.todo import TODO # noqa: E402
|
[ADD] script todo : installer les outils manquants, les quatre familles
La réduction sûre listait les outils manquants et s'arrêtait là. Trois
écritures séparées savaient installer, chacune un sous-ensemble
différent : openSUSE posait virt-viewer, mais ni navigateur CLI ni
lm-sensors.
Le binaire n'est presque jamais le paquet : sgdisk vit dans « gdisk »
chez Debian et Fedora, dans « gptfdisk » chez Arch et openSUSE. Deux
règles passent aussi dans la composante : l'ID de la distribution décide
avant le PATH, et la commande s'affiche avant la question.
Vérifié : commandes identiques sur les trois familles déjà couvertes,
4139 tests verts.
--- EN ---
The safe shrink listed the missing tools and stopped there. Three
separate writings knew how to install, each covering a different
subset: openSUSE could put virt-viewer down, but neither a CLI browser
nor lm-sensors.
A binary is almost never the package: sgdisk lives in "gdisk" on Debian
and Fedora, in "gptfdisk" on Arch and openSUSE. Two rules move into the
component as well: the distribution ID decides before the PATH, and the
command is shown before the question.
Checked: commands identical on the three families already covered, and
4139 tests green.
Assisted-by: Claude Opus 5
2026-08-31 07:18:13 -04:00
|
|
|
from script.todo.todo_i18n import t # noqa: E402
|
|
|
|
|
|
|
|
|
|
FAMILIES = ("apt-get", "dnf", "pacman", "zypper")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Exec:
|
|
|
|
|
"""Le lanceur de commandes, qui retient au lieu d'exécuter."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, status=0):
|
|
|
|
|
self.ran = []
|
|
|
|
|
self.status = status
|
|
|
|
|
|
|
|
|
|
def exec_command_live(self, cmd, source_erplibre=False):
|
|
|
|
|
self.ran.append(cmd)
|
|
|
|
|
return self.status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ShrinkToolsBase(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.todo.execute = _Exec()
|
|
|
|
|
|
|
|
|
|
def _which(self, package_manager, missing, installed_after=()):
|
|
|
|
|
"""shutil.which vu depuis qemu_manage : un seul gestionnaire de
|
|
|
|
|
paquets sur le PATH, et les outils manquants qui le restent tant que
|
|
|
|
|
l'installation n'a pas tourné."""
|
|
|
|
|
done = self.todo.execute
|
|
|
|
|
|
|
|
|
|
def which(binaire):
|
|
|
|
|
if binaire in FAMILIES:
|
|
|
|
|
return (
|
|
|
|
|
f"/usr/bin/{binaire}"
|
|
|
|
|
if binaire == package_manager
|
|
|
|
|
else None
|
|
|
|
|
)
|
|
|
|
|
if binaire in missing:
|
|
|
|
|
if done.ran and binaire in installed_after:
|
|
|
|
|
return f"/usr/bin/{binaire}"
|
|
|
|
|
return None
|
|
|
|
|
return f"/usr/bin/{binaire}"
|
|
|
|
|
|
|
|
|
|
return which
|
|
|
|
|
|
|
|
|
|
def _run(self, package_manager, missing, answer="y", installed_after=()):
|
|
|
|
|
which = self._which(package_manager, missing, installed_after)
|
|
|
|
|
buf = io.StringIO()
|
|
|
|
|
|
|
|
|
|
# input() écrit son invite sur stdout comme le vrai : sans cela la
|
|
|
|
|
# question n'apparaît nulle part et l'ORDRE des deux ne se voit pas.
|
|
|
|
|
def demande(invite=""):
|
|
|
|
|
print(invite, end="")
|
|
|
|
|
return answer
|
|
|
|
|
|
|
|
|
|
# `shutil` est UN seul objet module partagé : patcher son « which »
|
|
|
|
|
# par n'importe quel importateur le patche pour todo_install aussi,
|
|
|
|
|
# qui est le vrai lecteur du PATH depuis le refactor.
|
[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 (
|
|
|
|
|
patch("script.todo.qemu_manage.shutil.which", which),
|
|
|
|
|
patch("builtins.input", demande),
|
|
|
|
|
redirect_stdout(buf),
|
|
|
|
|
):
|
[ADD] script todo : installer les outils manquants, les quatre familles
La réduction sûre listait les outils manquants et s'arrêtait là. Trois
écritures séparées savaient installer, chacune un sous-ensemble
différent : openSUSE posait virt-viewer, mais ni navigateur CLI ni
lm-sensors.
Le binaire n'est presque jamais le paquet : sgdisk vit dans « gdisk »
chez Debian et Fedora, dans « gptfdisk » chez Arch et openSUSE. Deux
règles passent aussi dans la composante : l'ID de la distribution décide
avant le PATH, et la commande s'affiche avant la question.
Vérifié : commandes identiques sur les trois familles déjà couvertes,
4139 tests verts.
--- EN ---
The safe shrink listed the missing tools and stopped there. Three
separate writings knew how to install, each covering a different
subset: openSUSE could put virt-viewer down, but neither a CLI browser
nor lm-sensors.
A binary is almost never the package: sgdisk lives in "gdisk" on Debian
and Fedora, in "gptfdisk" on Arch and openSUSE. Two rules move into the
component as well: the distribution ID decides before the PATH, and the
command is shown before the question.
Checked: commands identical on the three families already covered, and
4139 tests green.
Assisted-by: Claude Opus 5
2026-08-31 07:18:13 -04:00
|
|
|
left = self.todo._qemu_install_shrink_tools(list(missing))
|
|
|
|
|
return self.todo.execute.ran, left, buf.getvalue()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestPackageTable(ShrinkToolsBase):
|
|
|
|
|
def test_every_tool_has_a_package_in_every_family(self):
|
|
|
|
|
"""Un trou ne plante pas : il propose une installation inutile."""
|
|
|
|
|
for family in FAMILIES:
|
|
|
|
|
per_family = TODO._SHRINK_PKG_FAMILY[family]
|
|
|
|
|
for binaire in TODO._SHRINK_TOOLS:
|
|
|
|
|
paquet = per_family.get(binaire) or TODO._SHRINK_PKG.get(
|
|
|
|
|
binaire
|
|
|
|
|
)
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
paquet,
|
|
|
|
|
f"{family} : aucun paquet connu pour « {binaire} »",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_the_overrides_cover_exactly_the_known_families(self):
|
|
|
|
|
"""Une famille connue de todo_install sans surcharge ici proposerait
|
|
|
|
|
« gdisk » à un Arch, qui ne l'a pas."""
|
|
|
|
|
self.assertEqual(set(TODO._SHRINK_PKG_FAMILY), set(FAMILIES))
|
|
|
|
|
self.assertEqual(set(todo_install.FAMILIES), set(FAMILIES))
|
|
|
|
|
|
|
|
|
|
def test_sgdisk_is_the_one_that_changes_name(self):
|
|
|
|
|
"""Le cas qui a motivé la table, gardé explicitement."""
|
|
|
|
|
noms = {f: TODO._SHRINK_PKG_FAMILY[f]["sgdisk"] for f in FAMILIES}
|
|
|
|
|
self.assertEqual(noms["apt-get"], "gdisk")
|
|
|
|
|
self.assertEqual(noms["dnf"], "gdisk")
|
|
|
|
|
self.assertEqual(noms["pacman"], "gptfdisk")
|
|
|
|
|
self.assertEqual(noms["zypper"], "gptfdisk")
|
|
|
|
|
|
|
|
|
|
def test_no_family_override_repeats_the_common_table(self):
|
|
|
|
|
"""Un doublon entre les deux tables est une divergence en attente."""
|
|
|
|
|
for family in FAMILIES:
|
|
|
|
|
for binaire in TODO._SHRINK_PKG_FAMILY[family]:
|
|
|
|
|
self.assertNotIn(
|
|
|
|
|
binaire,
|
|
|
|
|
TODO._SHRINK_PKG,
|
|
|
|
|
f"{family} : « {binaire} » est dans les deux tables",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestInstallCommand(ShrinkToolsBase):
|
|
|
|
|
def test_each_family_builds_its_own_command(self):
|
|
|
|
|
attendu = {
|
|
|
|
|
"apt-get": "sudo apt-get install -y gdisk",
|
|
|
|
|
"dnf": "sudo dnf install -y gdisk",
|
|
|
|
|
"pacman": "sudo pacman -S --needed --noconfirm gptfdisk",
|
|
|
|
|
"zypper": "sudo zypper --non-interactive install gptfdisk",
|
|
|
|
|
}
|
|
|
|
|
for family, cmd in attendu.items():
|
|
|
|
|
self.setUp()
|
|
|
|
|
ran, left, _ = self._run(
|
|
|
|
|
family, ["sgdisk"], installed_after=("sgdisk",)
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(ran, [cmd])
|
|
|
|
|
self.assertEqual(left, [])
|
|
|
|
|
|
|
|
|
|
def test_a_package_is_asked_for_once(self):
|
|
|
|
|
"""e2fsck, resize2fs et dumpe2fs sortent du même paquet."""
|
|
|
|
|
ran, _, _ = self._run(
|
|
|
|
|
"apt-get",
|
|
|
|
|
["e2fsck", "resize2fs", "dumpe2fs", "sgdisk"],
|
|
|
|
|
installed_after=("e2fsck", "resize2fs", "dumpe2fs", "sgdisk"),
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(ran, ["sudo apt-get install -y e2fsprogs gdisk"])
|
|
|
|
|
|
|
|
|
|
def test_the_command_is_shown_before_the_question(self):
|
|
|
|
|
"""On approuve ce qu'on a lu : la commande passe AVANT la question.
|
|
|
|
|
|
|
|
|
|
L'ordre est le fond de l'affaire, pas la simple présence des deux :
|
|
|
|
|
une question posée avant la commande fait approuver à l'aveugle.
|
|
|
|
|
"""
|
|
|
|
|
ran, _, out = self._run("apt-get", ["sgdisk"], answer="n")
|
|
|
|
|
commande = out.index("sudo apt-get install -y gdisk")
|
|
|
|
|
question = out.index(t("Install them? (y/N): "))
|
|
|
|
|
self.assertLess(commande, question)
|
|
|
|
|
self.assertEqual(ran, [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestGivingUp(ShrinkToolsBase):
|
|
|
|
|
def test_a_refusal_installs_nothing_and_keeps_the_list(self):
|
|
|
|
|
ran, left, _ = self._run("apt-get", ["sgdisk"], answer="n")
|
|
|
|
|
self.assertEqual(ran, [])
|
|
|
|
|
self.assertEqual(left, ["sgdisk"])
|
|
|
|
|
|
|
|
|
|
def test_an_unknown_package_manager_gives_up(self):
|
|
|
|
|
ran, left, _ = self._run("brew", ["sgdisk"])
|
|
|
|
|
self.assertEqual(ran, [])
|
|
|
|
|
self.assertEqual(left, ["sgdisk"])
|
|
|
|
|
|
|
|
|
|
def test_the_list_is_re_read_from_disk_not_assumed(self):
|
|
|
|
|
"""Une installation qui ne pose rien doit rester un échec."""
|
|
|
|
|
ran, left, _ = self._run("apt-get", ["sgdisk"], installed_after=())
|
|
|
|
|
self.assertEqual(len(ran), 1)
|
|
|
|
|
self.assertEqual(left, ["sgdisk"])
|
|
|
|
|
|
|
|
|
|
def test_a_failing_install_is_reported_and_not_swallowed(self):
|
|
|
|
|
"""exec_command_live REND le code de sortie, il ne lève rien."""
|
|
|
|
|
self.todo.execute = _Exec(status=100)
|
|
|
|
|
ran, left, out = self._run("apt-get", ["sgdisk"])
|
|
|
|
|
self.assertEqual(len(ran), 1)
|
|
|
|
|
self.assertEqual(left, ["sgdisk"])
|
|
|
|
|
self.assertIn("100", out)
|
|
|
|
|
|
|
|
|
|
|
2026-08-31 07:51:19 -04:00
|
|
|
class TestBackupSpace(unittest.TestCase):
|
|
|
|
|
"""La sauvegarde avant réduction, et la place qu'elle demande.
|
|
|
|
|
|
|
|
|
|
Elle doublait l'occupation sans rien annoncer : sur un disque presque
|
|
|
|
|
plein la copie s'arrête à mi-course et laisse un .bak tronqué, sur un
|
|
|
|
|
système de fichiers désormais saturé. Les deux chiffres passent donc
|
|
|
|
|
avant la question, et le défaut bascule quand la place manque — une
|
|
|
|
|
entrée distraite ne doit pas remplir le disque.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
GIB = 1 << 30
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
|
|
|
|
|
def test_the_need_is_the_allocated_size_not_the_apparent_one(self):
|
|
|
|
|
"""« cp --sparse=always » ne recopie pas les trous d'un qcow2 : un
|
|
|
|
|
disque de 60 Go apparents mais 8 Go alloués ne demande que 8 Go."""
|
|
|
|
|
faux = os.stat_result((0o644, 0, 0, 1, 0, 0, 60 * self.GIB, 0, 0, 0))
|
|
|
|
|
# st_blocks n'est pas dans le tuple : on le pose à part.
|
[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 (
|
|
|
|
|
patch("script.todo.qemu_manage.os.stat") as stat,
|
|
|
|
|
patch("script.todo.qemu_manage.shutil.disk_usage") as du,
|
|
|
|
|
):
|
2026-08-31 07:51:19 -04:00
|
|
|
stat.return_value = type(
|
|
|
|
|
"S", (), {"st_blocks": 8 * self.GIB // 512}
|
|
|
|
|
)()
|
|
|
|
|
du.return_value = type("U", (), {"free": 99 * self.GIB})()
|
|
|
|
|
besoin, libre = TODO._qemu_backup_need_and_free("/x/d.qcow2")
|
|
|
|
|
self.assertEqual(besoin, 8 * self.GIB)
|
|
|
|
|
self.assertEqual(libre, 99 * self.GIB)
|
|
|
|
|
self.assertNotEqual(besoin, faux.st_size)
|
|
|
|
|
|
|
|
|
|
def _decision(self, besoin, libre, answer):
|
|
|
|
|
"""(question posée, sauvegarde retenue) — par le VRAI code.
|
|
|
|
|
|
|
|
|
|
Ce helper appelle _qemu_ask_backup et ne réimplémente rien : une
|
|
|
|
|
copie de la logique dans le test aurait laissé passer un défaut
|
|
|
|
|
remis à OUI sans place, ce qui est précisément le défaut à garder.
|
|
|
|
|
"""
|
|
|
|
|
vu = []
|
|
|
|
|
|
|
|
|
|
def demande(invite=""):
|
|
|
|
|
vu.append(invite)
|
|
|
|
|
return answer
|
|
|
|
|
|
[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 (
|
|
|
|
|
patch.object(
|
|
|
|
|
TODO,
|
|
|
|
|
"_qemu_backup_need_and_free",
|
|
|
|
|
staticmethod(lambda d: (besoin, libre)),
|
|
|
|
|
),
|
|
|
|
|
patch("builtins.input", demande),
|
|
|
|
|
redirect_stdout(io.StringIO()),
|
|
|
|
|
):
|
2026-08-31 07:51:19 -04:00
|
|
|
retenu = self.todo._qemu_ask_backup("/x/d.qcow2")
|
|
|
|
|
return vu[-1], retenu
|
|
|
|
|
|
|
|
|
|
def test_with_room_the_default_stays_yes(self):
|
|
|
|
|
question, retenu = self._decision(12 * self.GIB, 40 * self.GIB, "")
|
|
|
|
|
self.assertIn("(O/n", question)
|
|
|
|
|
self.assertTrue(retenu)
|
|
|
|
|
|
|
|
|
|
def test_without_room_the_default_flips_to_no(self):
|
|
|
|
|
"""Le cœur du correctif : entrée vide ne doit PAS remplir le disque."""
|
|
|
|
|
question, retenu = self._decision(12 * self.GIB, 3 * self.GIB, "")
|
|
|
|
|
self.assertIn("(y/N", question)
|
|
|
|
|
self.assertFalse(retenu)
|
|
|
|
|
|
|
|
|
|
def test_without_room_insisting_still_works(self):
|
|
|
|
|
"""On informe, on ne décide pas à la place de l'opérateur."""
|
|
|
|
|
_, retenu = self._decision(12 * self.GIB, 3 * self.GIB, "y")
|
|
|
|
|
self.assertTrue(retenu)
|
|
|
|
|
|
|
|
|
|
def test_a_margin_guards_the_exactly_equal_case(self):
|
|
|
|
|
"""Une place égale au besoin n'en laisse aucune : refusé."""
|
|
|
|
|
_, retenu = self._decision(12 * self.GIB, 12 * self.GIB, "")
|
|
|
|
|
self.assertFalse(retenu)
|
|
|
|
|
|
|
|
|
|
|
[ADD] script todo : installer les outils manquants, les quatre familles
La réduction sûre listait les outils manquants et s'arrêtait là. Trois
écritures séparées savaient installer, chacune un sous-ensemble
différent : openSUSE posait virt-viewer, mais ni navigateur CLI ni
lm-sensors.
Le binaire n'est presque jamais le paquet : sgdisk vit dans « gdisk »
chez Debian et Fedora, dans « gptfdisk » chez Arch et openSUSE. Deux
règles passent aussi dans la composante : l'ID de la distribution décide
avant le PATH, et la commande s'affiche avant la question.
Vérifié : commandes identiques sur les trois familles déjà couvertes,
4139 tests verts.
--- EN ---
The safe shrink listed the missing tools and stopped there. Three
separate writings knew how to install, each covering a different
subset: openSUSE could put virt-viewer down, but neither a CLI browser
nor lm-sensors.
A binary is almost never the package: sgdisk lives in "gdisk" on Debian
and Fedora, in "gptfdisk" on Arch and openSUSE. Two rules move into the
component as well: the distribution ID decides before the PATH, and the
command is shown before the question.
Checked: commands identical on the three families already covered, and
4139 tests green.
Assisted-by: Claude Opus 5
2026-08-31 07:18:13 -04:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|