Poser le binaire ne change pas le prompt : c'est la ligne d'initialisation dans le fichier du shell qui le fait, et les deux étapes échouent séparément. Le paquet vient de la distribution quand elle le connaît, de l'installateur amont sinon — starship n'est pas empaqueté partout. Le fichier du shell n'est demandé que devant plusieurs candidats, et la ligne ne s'écrit qu'une fois. L'entrée porte sa destination dans « method » : son rang suit le nombre d'entrées de todo.json, qu'un numéro codé en dur ignorerait. Vérifié : 18 tests. --- EN --- Laying down the binary does not change the prompt: the init line in the shell file does, and the two steps fail separately. The package comes from the distribution when it knows it, from the upstream installer otherwise — starship is not packaged everywhere. The shell file is only asked for when several candidates exist, and the line is written only once. The entry carries its destination in « method »: its rank follows the number of todo.json entries, which a hard-coded number would ignore. Checked: 18 tests. Assisted-by: Claude Opus 5
237 lines
8.1 KiB
Python
237 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()
|