238 lines
8.1 KiB
Python
238 lines
8.1 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||
|
|
|
||
|
|
"""Installer starship : deux étapes qui échouent séparément.
|
||
|
|
|
||
|
|
Poser le binaire ne change pas le prompt ; c'est la ligne d'initialisation
|
||
|
|
dans le fichier du shell qui le fait. Un menu qui confond les deux annonce un
|
||
|
|
succès devant un prompt inchangé, ou réécrit la ligne à chaque passage.
|
||
|
|
|
||
|
|
Le fichier à modifier ne se demande que devant un vrai choix : plusieurs
|
||
|
|
fichiers de configuration présents. Aucun, ou un seul, ne laisse rien à
|
||
|
|
trancher — une question posée là n'attend qu'une frappe pour rien.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import io
|
||
|
|
import os
|
||
|
|
import tempfile
|
||
|
|
import unittest
|
||
|
|
from contextlib import redirect_stdout
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import MagicMock, patch
|
||
|
|
|
||
|
|
from script.todo.todo import TODO
|
||
|
|
|
||
|
|
LIGNE_BASH = 'eval "$(starship init bash)"'
|
||
|
|
LIGNE_ZSH = 'eval "$(starship init zsh)"'
|
||
|
|
|
||
|
|
|
||
|
|
def refuse_input(*args, **kwargs):
|
||
|
|
raise AssertionError("aucune question ne devait être posée")
|
||
|
|
|
||
|
|
|
||
|
|
class TestShellName(unittest.TestCase):
|
||
|
|
"""Le shell se lit dans $SHELL, dont seul le nom de base compte."""
|
||
|
|
|
||
|
|
def test_basename_of_shell(self):
|
||
|
|
with patch.dict(os.environ, {"SHELL": "/usr/bin/zsh"}):
|
||
|
|
self.assertEqual(TODO._shell_name(), "zsh")
|
||
|
|
|
||
|
|
def test_empty_when_unset(self):
|
||
|
|
with patch.dict(os.environ, {"SHELL": ""}):
|
||
|
|
self.assertEqual(TODO._shell_name(), "")
|
||
|
|
|
||
|
|
|
||
|
|
class ShellFixture(unittest.TestCase):
|
||
|
|
"""Un faux HOME où l'on pose les fichiers de configuration voulus."""
|
||
|
|
|
||
|
|
def setUp(self):
|
||
|
|
self.tmp = tempfile.TemporaryDirectory()
|
||
|
|
self.addCleanup(self.tmp.cleanup)
|
||
|
|
racine = Path(self.tmp.name)
|
||
|
|
self.fichiers = {
|
||
|
|
"bash": racine / "bashrc",
|
||
|
|
"zsh": racine / "zshrc",
|
||
|
|
"fish": racine / "fish" / "config.fish",
|
||
|
|
}
|
||
|
|
table = {
|
||
|
|
"bash": (str(self.fichiers["bash"]), LIGNE_BASH),
|
||
|
|
"zsh": (str(self.fichiers["zsh"]), LIGNE_ZSH),
|
||
|
|
"fish": (
|
||
|
|
str(self.fichiers["fish"]),
|
||
|
|
"starship init fish | source",
|
||
|
|
),
|
||
|
|
}
|
||
|
|
patcher = patch.dict(TODO._STARSHIP_SHELLS, table, clear=True)
|
||
|
|
patcher.start()
|
||
|
|
self.addCleanup(patcher.stop)
|
||
|
|
self.todo = TODO()
|
||
|
|
|
||
|
|
def cree(self, *shells, contenu=""):
|
||
|
|
for shell in shells:
|
||
|
|
chemin = self.fichiers[shell]
|
||
|
|
chemin.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
chemin.write_text(contenu, encoding="utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
class TestStarshipTarget(ShellFixture):
|
||
|
|
"""Quel fichier, et quand la question se pose."""
|
||
|
|
|
||
|
|
def choisit(self, shell_env="/bin/bash", reponse=None):
|
||
|
|
entree = refuse_input if reponse is None else (lambda *a: reponse)
|
||
|
|
with patch.dict(os.environ, {"SHELL": shell_env}), patch(
|
||
|
|
"builtins.input", entree
|
||
|
|
), redirect_stdout(io.StringIO()):
|
||
|
|
return self.todo._shell_starship_target()
|
||
|
|
|
||
|
|
def test_no_file_means_bash_without_asking(self):
|
||
|
|
self.assertEqual(self.choisit(), "bash")
|
||
|
|
|
||
|
|
def test_a_single_file_is_taken_without_asking(self):
|
||
|
|
self.cree("zsh")
|
||
|
|
self.assertEqual(self.choisit(), "zsh")
|
||
|
|
|
||
|
|
def test_a_single_file_wins_over_the_current_shell(self):
|
||
|
|
"""Le fichier présent l'emporte : $SHELL n'en a pas, lui."""
|
||
|
|
self.cree("fish")
|
||
|
|
self.assertEqual(self.choisit(shell_env="/bin/bash"), "fish")
|
||
|
|
|
||
|
|
def test_several_files_ask_and_default_to_the_current_shell(self):
|
||
|
|
self.cree("bash", "zsh")
|
||
|
|
self.assertEqual(
|
||
|
|
self.choisit(shell_env="/usr/bin/zsh", reponse=""), "zsh"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_several_files_answered_by_number(self):
|
||
|
|
self.cree("bash", "zsh")
|
||
|
|
self.assertEqual(self.choisit(reponse="2"), "zsh")
|
||
|
|
|
||
|
|
def test_several_files_answered_by_name(self):
|
||
|
|
self.cree("bash", "zsh")
|
||
|
|
self.assertEqual(self.choisit(reponse="zsh"), "zsh")
|
||
|
|
|
||
|
|
def test_a_nonsense_answer_falls_back_on_the_default(self):
|
||
|
|
self.cree("bash", "zsh")
|
||
|
|
self.assertEqual(self.choisit(reponse="42"), "bash")
|
||
|
|
|
||
|
|
|
||
|
|
class TestHookStarship(ShellFixture):
|
||
|
|
"""L'écriture de la ligne, qui ne demande rien."""
|
||
|
|
|
||
|
|
def hook(self, shell_env="/bin/bash"):
|
||
|
|
out = io.StringIO()
|
||
|
|
with patch.dict(os.environ, {"SHELL": shell_env}), patch(
|
||
|
|
"builtins.input", refuse_input
|
||
|
|
), redirect_stdout(out):
|
||
|
|
self.todo._shell_hook_starship()
|
||
|
|
return out.getvalue()
|
||
|
|
|
||
|
|
def test_it_appends_the_line(self):
|
||
|
|
self.cree("bash", contenu="export EDITOR=vim\n")
|
||
|
|
self.hook()
|
||
|
|
self.assertEqual(
|
||
|
|
self.fichiers["bash"].read_text(encoding="utf-8"),
|
||
|
|
f"export EDITOR=vim\n{LIGNE_BASH}\n",
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_a_file_without_a_final_newline_keeps_its_last_command(self):
|
||
|
|
self.cree("bash", contenu="export EDITOR=vim")
|
||
|
|
self.hook()
|
||
|
|
self.assertEqual(
|
||
|
|
self.fichiers["bash"].read_text(encoding="utf-8"),
|
||
|
|
f"export EDITOR=vim\n{LIGNE_BASH}\n",
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_it_writes_only_once(self):
|
||
|
|
self.cree("bash", contenu=f"{LIGNE_BASH}\n")
|
||
|
|
sortie = self.hook()
|
||
|
|
self.assertEqual(
|
||
|
|
self.fichiers["bash"].read_text(encoding="utf-8"),
|
||
|
|
f"{LIGNE_BASH}\n",
|
||
|
|
)
|
||
|
|
self.assertIn("✅", sortie)
|
||
|
|
|
||
|
|
def test_no_file_at_all_creates_the_bash_one(self):
|
||
|
|
self.hook()
|
||
|
|
self.assertEqual(
|
||
|
|
self.fichiers["bash"].read_text(encoding="utf-8"),
|
||
|
|
f"{LIGNE_BASH}\n",
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_a_missing_parent_directory_is_created(self):
|
||
|
|
"""config.fish vit sous ~/.config/fish, que rien ne garantit."""
|
||
|
|
self.todo._STARSHIP_SHELLS["bash"] = (
|
||
|
|
str(self.fichiers["fish"]),
|
||
|
|
LIGNE_BASH,
|
||
|
|
)
|
||
|
|
self.hook()
|
||
|
|
self.assertEqual(
|
||
|
|
self.fichiers["fish"].read_text(encoding="utf-8"),
|
||
|
|
f"{LIGNE_BASH}\n",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestInstallStarship(unittest.TestCase):
|
||
|
|
"""L'enchaînement des deux étapes, et ce qui l'arrête."""
|
||
|
|
|
||
|
|
def test_an_installed_binary_goes_straight_to_the_shell(self):
|
||
|
|
todo = TODO()
|
||
|
|
todo.execute = MagicMock()
|
||
|
|
with patch(
|
||
|
|
"script.todo.todo.shutil.which", return_value="/usr/bin/starship"
|
||
|
|
), patch.object(TODO, "_shell_hook_starship") as hook, patch.object(
|
||
|
|
TODO, "_shell_install_starship_binary"
|
||
|
|
) as poser:
|
||
|
|
todo._shell_install_starship()
|
||
|
|
poser.assert_not_called()
|
||
|
|
hook.assert_called_once()
|
||
|
|
|
||
|
|
def test_a_missing_binary_leaves_the_shell_alone(self):
|
||
|
|
todo = TODO()
|
||
|
|
todo.execute = MagicMock()
|
||
|
|
out = io.StringIO()
|
||
|
|
with patch(
|
||
|
|
"script.todo.todo.shutil.which", return_value=None
|
||
|
|
), patch.object(TODO, "_shell_hook_starship") as hook, patch.object(
|
||
|
|
TODO, "_shell_install_starship_binary"
|
||
|
|
), redirect_stdout(
|
||
|
|
out
|
||
|
|
):
|
||
|
|
todo._shell_install_starship()
|
||
|
|
hook.assert_not_called()
|
||
|
|
self.assertIn("❌", out.getvalue())
|
||
|
|
|
||
|
|
def test_a_refused_package_does_not_chain_to_upstream(self):
|
||
|
|
"""Un refus est une décision : il n'appelle pas une seconde offre."""
|
||
|
|
todo = TODO()
|
||
|
|
todo.execute = MagicMock()
|
||
|
|
appels = []
|
||
|
|
with patch(
|
||
|
|
"script.todo.todo.todo_install.install_command",
|
||
|
|
return_value=["sudo", "pacman", "-S", "starship"],
|
||
|
|
), patch(
|
||
|
|
"script.todo.todo.todo_install.ask_and_install",
|
||
|
|
side_effect=lambda *a, **k: appels.append(a[1]) or None,
|
||
|
|
):
|
||
|
|
todo._shell_install_starship_binary()
|
||
|
|
self.assertEqual(len(appels), 1)
|
||
|
|
|
||
|
|
def test_no_package_falls_back_upstream(self):
|
||
|
|
todo = TODO()
|
||
|
|
todo.execute = MagicMock()
|
||
|
|
appels = []
|
||
|
|
with patch(
|
||
|
|
"script.todo.todo.todo_install.install_command", return_value=None
|
||
|
|
), patch(
|
||
|
|
"script.todo.todo.todo_install.ask_and_install",
|
||
|
|
side_effect=lambda *a, **k: appels.append(a[1]) or 0,
|
||
|
|
), redirect_stdout(
|
||
|
|
io.StringIO()
|
||
|
|
):
|
||
|
|
todo._shell_install_starship_binary()
|
||
|
|
self.assertEqual(appels, [TODO._STARSHIP_UPSTREAM])
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|