erplibre/test/test_todo_install.py

201 lines
7.6 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""La composante d'installation partagée : familles, noms, ordre de décision.
Trois écritures séparées faisaient ce travail avant, chacune couvrant un
sous-ensemble différent : on pouvait installer virt-viewer sous openSUSE, mais
ni navigateur CLI ni lm-sensors. Ce qui se garde ici est donc surtout de la
COUVERTURE — qu'aucune famille ne retombe dans le trou — plus les deux règles
que la composante tient à la place des appelants : l'ID de la distribution
décide avant le PATH, et la commande s'affiche avant la question.
Rien n'est installé : le PATH et /etc/os-release sont simulés.
"""
import io
import sys
import unittest
from contextlib import redirect_stdout
from unittest.mock import mock_open, patch
sys.argv = ["todo.py"]
from script.todo import todo_install # noqa: E402
def _only(*present):
"""shutil.which qui ne trouve que `present`."""
return lambda b: f"/usr/bin/{b}" if b in present else None
class TestFamilyDetection(unittest.TestCase):
def test_every_family_has_a_non_interactive_command(self):
"""Un menu qui rend la main à un prompt apt reste bloqué."""
for famille in todo_install.FAMILIES:
cmd = todo_install.install_command(["p"], famille=famille)
self.assertEqual(cmd[0], "sudo")
self.assertEqual(cmd[-1], "p")
self.assertTrue(
{"-y", "--noconfirm", "--non-interactive"} & set(cmd),
f"{famille} : commande interactive {cmd}",
)
def test_the_distribution_id_decides_before_the_path(self):
"""Une machine peut porter deux gestionnaires ; l'ID dit lequel
possède le système."""
with patch(
"builtins.open", mock_open(read_data='ID="debian"\n')
), patch(
"script.todo.todo_install.shutil.which", _only("apt-get", "dnf")
):
self.assertEqual(todo_install.family(), "apt-get")
def test_the_path_decides_when_the_id_is_unknown(self):
with patch(
"builtins.open", mock_open(read_data='ID="nonesuch"\n')
), patch("script.todo.todo_install.shutil.which", _only("zypper")):
self.assertEqual(todo_install.family(), "zypper")
def test_an_id_whose_manager_is_absent_falls_back(self):
"""Un conteneur Debian minimal sans apt-get ne doit pas mener à une
commande apt-get qui n'existe pas."""
with patch(
"builtins.open", mock_open(read_data='ID="debian"\n')
), patch("script.todo.todo_install.shutil.which", _only("dnf")):
self.assertEqual(todo_install.family(), "dnf")
def test_no_manager_is_none_not_a_guess(self):
with patch("builtins.open", mock_open(read_data="")), patch(
"script.todo.todo_install.shutil.which", _only()
):
self.assertIsNone(todo_install.family())
self.assertIsNone(todo_install.install_command(["p"]))
def test_the_four_supported_families_are_all_mapped(self):
"""Les plateformes annoncées par le dépôt doivent toutes tomber sur
une famille, sans quoi l'installation leur est fermée."""
for os_id, attendu in (
("ubuntu", "apt-get"),
("linuxmint", "apt-get"),
("debian", "apt-get"),
("almalinux", "dnf"),
("rocky", "dnf"),
("opensuse-leap", "zypper"),
("opensuse-tumbleweed", "zypper"),
("arch", "pacman"),
):
with patch(
"builtins.open", mock_open(read_data=f'ID="{os_id}"\n')
), patch(
"script.todo.todo_install.shutil.which",
_only(*todo_install.FAMILIES),
):
self.assertEqual(todo_install.family(), attendu, os_id)
class TestPackageNames(unittest.TestCase):
def test_a_dict_picks_the_name_of_the_current_family(self):
"""lm-sensors chez Debian, lm_sensors ailleurs."""
noms = {
"apt-get": ["lm-sensors"],
"dnf": ["lm_sensors"],
"pacman": ["lm_sensors"],
"zypper": ["sensors"],
}
for famille, attendu in (
("apt-get", "lm-sensors"),
("dnf", "lm_sensors"),
("zypper", "sensors"),
):
cmd = todo_install.install_command(noms, famille=famille)
self.assertEqual(cmd[-1], attendu)
def test_a_family_absent_from_the_dict_installs_nothing(self):
"""Mieux vaut rien proposer qu'un paquet qui n'existe pas ici."""
self.assertIsNone(
todo_install.install_command({"apt-get": ["p"]}, famille="pacman")
)
def test_resolve_maps_binaries_and_deduplicates(self):
paquets, inconnus = todo_install.resolve(
["e2fsck", "resize2fs", "sgdisk"],
commun={"e2fsck": "e2fsprogs", "resize2fs": "e2fsprogs"},
par_famille={"pacman": {"sgdisk": "gptfdisk"}},
famille="pacman",
)
self.assertEqual(paquets, ["e2fsprogs", "gptfdisk"])
self.assertEqual(inconnus, [])
def test_resolve_says_what_it_cannot_map(self):
"""À dire, jamais à deviner : un nom inventé installerait au hasard."""
paquets, inconnus = todo_install.resolve(
["sgdisk"], commun={}, par_famille={}, famille="apt-get"
)
self.assertEqual(paquets, [])
self.assertEqual(inconnus, ["sgdisk"])
class TestAskAndInstall(unittest.TestCase):
class _Exec:
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
def _play(self, cmd, answer, status=0):
ex = self._Exec(status)
buf = io.StringIO()
def demande(invite=""):
print(invite, end="")
return answer
with patch("builtins.input", demande), redirect_stdout(buf):
got = todo_install.ask_and_install(
ex, cmd, "Installer? (y/N): ", lambda a: a.strip() == "y"
)
return ex.ran, got, buf.getvalue()
def test_the_command_is_printed_before_the_question(self):
"""Centralisé ici pour qu'aucun appelant ne puisse l'inverser."""
ran, got, out = self._play(
["sudo", "apt-get", "install", "-y", "p"], "n"
)
self.assertLess(
out.index("sudo apt-get install -y p"),
out.index("Installer? (y/N): "),
)
self.assertEqual(ran, [])
self.assertIsNone(got)
def test_accepting_runs_it_and_gives_the_exit_code_back(self):
"""exec_command_live REND le code, il ne lève rien : l'appelant doit
pouvoir le tester."""
ran, got, _ = self._play(["sudo", "dnf", "install", "-y", "p"], "y")
self.assertEqual(ran, ["sudo dnf install -y p"])
self.assertEqual(got, 0)
ran, got, _ = self._play(
["sudo", "dnf", "install", "-y", "p"], "y", status=127
)
self.assertEqual(got, 127)
def test_no_command_is_said_not_silently_skipped(self):
ran, got, out = self._play(None, "y")
self.assertEqual(ran, [])
self.assertIsNone(got)
self.assertTrue(out.strip())
def test_a_name_with_a_space_survives_the_display(self):
"""shlex.join, pour que la commande affichée soit celle qui tourne."""
ran, _, out = self._play(
["sudo", "apt-get", "install", "-y", "a b"], "y"
)
self.assertIn("'a b'", out)
self.assertIn("'a b'", ran[0])
if __name__ == "__main__":
unittest.main()