[FIX] script todo : rtk hors du PATH, résultat d'installation annoncé
Un processus garde le PATH qu'il avait au démarrage : rtk installé dans ~/.local/bin pendant que TODO tourne échappe à shutil.which, et « rtk » nu sort en 127. Le menu le cherche donc aussi à l'emplacement de l'installateur et l'appelle par son chemin absolu, sans confondre un binaire hors PATH avec une absence. L'installation annonce son résultat : version et chemin, ou échec. Vérifié : 9 tests neufs, 117 au total. --- EN --- A process keeps the PATH it had at startup: rtk installed into ~/.local/bin while TODO runs escapes shutil.which, and a bare « rtk » exits 127. The menu therefore also looks at the installer's location and calls the binary by its absolute path, without mistaking a binary outside the PATH for a missing one. Installation now reports its outcome: version and path, or failure. Checked: 9 new tests, 117 in total. Assisted-by: Claude Opus 5
This commit is contained in:
parent
1cc74c83b4
commit
3dcccf40d9
3 changed files with 249 additions and 38 deletions
|
|
@ -4227,6 +4227,83 @@ class TODO(
|
|||
else:
|
||||
print(t("Command not found !"))
|
||||
|
||||
def rtk_locate(self):
|
||||
"""Localise l'exécutable rtk.
|
||||
|
||||
Rend le couple (chemin, visible_dans_le_PATH). Le chemin vaut None
|
||||
quand rtk est introuvable. Le second membre est faux quand le binaire
|
||||
existe à l'emplacement où l'installateur le dépose sans que le PATH y
|
||||
mène : un processus garde le PATH qu'il avait au démarrage, donc une
|
||||
installation faite pendant que TODO tourne lui reste invisible tant
|
||||
qu'il n'est pas relancé.
|
||||
"""
|
||||
rtk_path = shutil.which("rtk")
|
||||
if rtk_path:
|
||||
return rtk_path, True
|
||||
# Emplacement par défaut de l'installateur (RTK_INSTALL_DIR le change).
|
||||
fallback = os.path.expanduser("~/.local/bin/rtk")
|
||||
if os.access(fallback, os.X_OK):
|
||||
return fallback, False
|
||||
return None, False
|
||||
|
||||
def rtk_exec(self, args):
|
||||
"""Lance rtk par son chemin absolu, ou signale qu'il est absent.
|
||||
|
||||
Le chemin absolu évite le code 127 d'un « rtk » nu quand le PATH du
|
||||
processus ne mène pas à l'emplacement d'installation.
|
||||
"""
|
||||
rtk_path, _ = self.rtk_locate()
|
||||
if rtk_path is None:
|
||||
print(t("RTK is not installed. Use option 1 to install it."))
|
||||
return 1
|
||||
return self.execute.exec_command_live(
|
||||
f"{shlex.quote(rtk_path)} {args}",
|
||||
source_erplibre=False,
|
||||
)
|
||||
|
||||
def rtk_version(self, rtk_path):
|
||||
"""Rend la version qu'annonce le binaire, « ? » s'il ne répond pas."""
|
||||
result = self.execute.exec_command_live(
|
||||
f"{shlex.quote(rtk_path)} --version",
|
||||
source_erplibre=False,
|
||||
quiet=True,
|
||||
return_status_and_output=True,
|
||||
)
|
||||
if isinstance(result, tuple) and result[0] == 0:
|
||||
return " ".join(result[1]).strip()
|
||||
return "?"
|
||||
|
||||
def rtk_report_path_warning(self):
|
||||
"""Dit comment rendre rtk appelable quand le PATH ne le porte pas."""
|
||||
print(t("rtk is not in the PATH of this process, restart TODO."))
|
||||
print(t("To make it permanent, add to your shell profile:"))
|
||||
print(' export PATH="$HOME/.local/bin:$PATH"')
|
||||
|
||||
def rtk_report_install(self, exit_code):
|
||||
"""Annonce le résultat de l'installation, PATH compris.
|
||||
|
||||
Une installation réussie ne rend pas rtk appelable pour autant : le
|
||||
binaire atterrit dans un répertoire que le PATH du processus courant
|
||||
peut ignorer. Distinguer les deux cas évite de conclure à un échec
|
||||
devant un « commande introuvable » qui ne tient qu'au PATH.
|
||||
"""
|
||||
if exit_code:
|
||||
print(f"❌ {t('RTK installation failed, see the output above.')}")
|
||||
return
|
||||
rtk_path, in_path = self.rtk_locate()
|
||||
if rtk_path is None:
|
||||
print(
|
||||
"❌"
|
||||
f" {t('Installation ended without error, but no rtk binary was found.')}"
|
||||
)
|
||||
return
|
||||
print(
|
||||
f"✅ {t('RTK is installed, version: ')}{self.rtk_version(rtk_path)}"
|
||||
)
|
||||
print(f" {rtk_path}")
|
||||
if not in_path:
|
||||
self.rtk_report_path_warning()
|
||||
|
||||
def rtk_install(self):
|
||||
print(f"🤖 {t('Installation method:')}")
|
||||
choices = [
|
||||
|
|
@ -4244,64 +4321,47 @@ class TODO(
|
|||
if status == "0":
|
||||
return
|
||||
elif status == "1":
|
||||
self.execute.exec_command_live(
|
||||
"curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh",
|
||||
source_erplibre=False,
|
||||
command = (
|
||||
"curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/"
|
||||
"refs/heads/master/install.sh | sh"
|
||||
)
|
||||
elif status == "2":
|
||||
self.execute.exec_command_live(
|
||||
"brew install rtk",
|
||||
source_erplibre=False,
|
||||
)
|
||||
command = "brew install rtk"
|
||||
elif status == "3":
|
||||
self.execute.exec_command_live(
|
||||
"cargo install --git https://github.com/rtk-ai/rtk",
|
||||
source_erplibre=False,
|
||||
)
|
||||
command = "cargo install --git https://github.com/rtk-ai/rtk"
|
||||
else:
|
||||
print(t("Command not found !"))
|
||||
return
|
||||
exit_code = self.execute.exec_command_live(
|
||||
command,
|
||||
source_erplibre=False,
|
||||
)
|
||||
self.rtk_report_install(exit_code)
|
||||
|
||||
def rtk_check_version(self):
|
||||
self.execute.exec_command_live(
|
||||
"rtk --version",
|
||||
source_erplibre=False,
|
||||
)
|
||||
self.rtk_exec("--version")
|
||||
|
||||
def rtk_show_gain(self):
|
||||
self.execute.exec_command_live(
|
||||
"rtk gain",
|
||||
source_erplibre=False,
|
||||
)
|
||||
self.rtk_exec("gain")
|
||||
|
||||
def rtk_discover(self):
|
||||
self.execute.exec_command_live(
|
||||
"rtk discover",
|
||||
source_erplibre=False,
|
||||
)
|
||||
self.rtk_exec("discover")
|
||||
|
||||
def rtk_init_global(self):
|
||||
self.execute.exec_command_live(
|
||||
"rtk init --global",
|
||||
source_erplibre=False,
|
||||
)
|
||||
self.rtk_exec("init --global")
|
||||
|
||||
def rtk_check_status(self):
|
||||
rtk_path = shutil.which("rtk")
|
||||
rtk_path, in_path = self.rtk_locate()
|
||||
if rtk_path is None:
|
||||
print(t("RTK is not installed. Use option 1 to install it."))
|
||||
return
|
||||
|
||||
result = self.execute.exec_command_live(
|
||||
"rtk --version",
|
||||
source_erplibre=False,
|
||||
quiet=True,
|
||||
return_status_and_output=True,
|
||||
print(
|
||||
f"{t('RTK is installed, version: ')}{self.rtk_version(rtk_path)}"
|
||||
)
|
||||
if isinstance(result, tuple) and result[0] == 0:
|
||||
version_output = " ".join(result[1]).strip()
|
||||
print(f"{t('RTK is installed, version: ')}{version_output}")
|
||||
else:
|
||||
print(f"{t('RTK is installed, version: ')}?")
|
||||
print(f" {rtk_path}")
|
||||
if not in_path:
|
||||
self.rtk_report_path_warning()
|
||||
|
||||
config_path = os.path.expanduser("~/.config/rtk/config.toml")
|
||||
if os.path.exists(config_path):
|
||||
|
|
|
|||
|
|
@ -367,6 +367,25 @@ TRANSLATIONS = {
|
|||
"fr": "RTK est installé, version : ",
|
||||
"en": "RTK is installed, version: ",
|
||||
},
|
||||
"RTK installation failed, see the output above.": {
|
||||
"fr": "L'installation de RTK a échoué, voir la sortie ci-dessus.",
|
||||
"en": "RTK installation failed, see the output above.",
|
||||
},
|
||||
"Installation ended without error, but no rtk binary was found.": {
|
||||
"fr": (
|
||||
"L'installation s'est terminée sans erreur, mais aucun binaire"
|
||||
" rtk n'a été trouvé."
|
||||
),
|
||||
"en": "Installation ended without error, but no rtk binary was found.",
|
||||
},
|
||||
"rtk is not in the PATH of this process, restart TODO.": {
|
||||
"fr": "rtk n'est pas dans le PATH de ce processus, relancer TODO.",
|
||||
"en": "rtk is not in the PATH of this process, restart TODO.",
|
||||
},
|
||||
"To make it permanent, add to your shell profile:": {
|
||||
"fr": "Pour que ce soit permanent, ajouter au profil du shell :",
|
||||
"en": "To make it permanent, add to your shell profile:",
|
||||
},
|
||||
"Global auto-rewrite hook: active": {
|
||||
"fr": "Hook auto-rewrite global : actif",
|
||||
"en": "Global auto-rewrite hook: active",
|
||||
|
|
|
|||
132
test/test_todo_rtk.py
Normal file
132
test/test_todo_rtk.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
"""Le menu RTK : dit-il ce qui s'est réellement passé ?
|
||||
|
||||
Deux modes de défaillance se ressemblent à l'écran et n'ont pas le même
|
||||
remède. Le binaire peut être absent — l'installation a échoué. Il peut aussi
|
||||
être posé sur le disque sans que le PATH du processus y mène : un processus
|
||||
garde le PATH qu'il avait au démarrage, donc une installation faite pendant
|
||||
que TODO tourne lui reste invisible jusqu'au redémarrage. Lancer « rtk » nu
|
||||
rend alors 127, que rien ne distingue d'une absence.
|
||||
|
||||
Ce test vérifie que les deux cas sont annoncés séparément, et que les
|
||||
commandes passent par le chemin absolu du binaire plutôt que par le PATH.
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from script.todo.todo import TODO
|
||||
|
||||
FALLBACK = os.path.expanduser("~/.local/bin/rtk")
|
||||
PATH_HINT = 'export PATH="$HOME/.local/bin:$PATH"'
|
||||
|
||||
|
||||
class TestRtkLocate(unittest.TestCase):
|
||||
"""rtk_locate distingue « dans le PATH », « posé ailleurs » et « absent »."""
|
||||
|
||||
def test_found_in_path(self):
|
||||
with patch(
|
||||
"script.todo.todo.shutil.which", return_value="/usr/bin/rtk"
|
||||
):
|
||||
self.assertEqual(TODO().rtk_locate(), ("/usr/bin/rtk", True))
|
||||
|
||||
def test_found_outside_path(self):
|
||||
with patch("script.todo.todo.shutil.which", return_value=None), patch(
|
||||
"script.todo.todo.os.access", return_value=True
|
||||
):
|
||||
self.assertEqual(TODO().rtk_locate(), (FALLBACK, False))
|
||||
|
||||
def test_absent(self):
|
||||
with patch("script.todo.todo.shutil.which", return_value=None), patch(
|
||||
"script.todo.todo.os.access", return_value=False
|
||||
):
|
||||
self.assertEqual(TODO().rtk_locate(), (None, False))
|
||||
|
||||
|
||||
class TestRtkExec(unittest.TestCase):
|
||||
"""rtk_exec appelle le binaire par son chemin absolu, jamais « rtk » nu."""
|
||||
|
||||
def test_uses_absolute_path(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
todo.execute.exec_command_live.return_value = 0
|
||||
with patch("script.todo.todo.shutil.which", return_value=None), patch(
|
||||
"script.todo.todo.os.access", return_value=True
|
||||
):
|
||||
todo.rtk_exec("gain")
|
||||
command = todo.execute.exec_command_live.call_args[0][0]
|
||||
self.assertTrue(command.startswith(FALLBACK), command)
|
||||
self.assertTrue(command.endswith(" gain"), command)
|
||||
|
||||
def test_absent_runs_nothing(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
with patch("script.todo.todo.shutil.which", return_value=None), patch(
|
||||
"script.todo.todo.os.access", return_value=False
|
||||
):
|
||||
with redirect_stdout(io.StringIO()):
|
||||
status = todo.rtk_exec("gain")
|
||||
self.assertEqual(status, 1)
|
||||
todo.execute.exec_command_live.assert_not_called()
|
||||
|
||||
|
||||
class TestRtkReportInstall(unittest.TestCase):
|
||||
"""Le compte rendu d'installation nomme le résultat, sans le supposer."""
|
||||
|
||||
def report(self, todo, exit_code):
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
todo.rtk_report_install(exit_code)
|
||||
return out.getvalue()
|
||||
|
||||
def test_failure_is_not_announced_as_success(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
output = self.report(todo, 1)
|
||||
self.assertIn("❌", output)
|
||||
self.assertNotIn("✅", output)
|
||||
todo.execute.exec_command_live.assert_not_called()
|
||||
|
||||
def test_success_reports_version_and_path(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
todo.execute.exec_command_live.return_value = (0, ["rtk 0.47.0"])
|
||||
with patch(
|
||||
"script.todo.todo.shutil.which", return_value="/usr/bin/rtk"
|
||||
):
|
||||
output = self.report(todo, 0)
|
||||
self.assertIn("✅", output)
|
||||
self.assertIn("rtk 0.47.0", output)
|
||||
self.assertIn("/usr/bin/rtk", output)
|
||||
self.assertNotIn(PATH_HINT, output)
|
||||
|
||||
def test_success_outside_path_tells_how_to_reach_it(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
todo.execute.exec_command_live.return_value = (0, ["rtk 0.47.0"])
|
||||
with patch("script.todo.todo.shutil.which", return_value=None), patch(
|
||||
"script.todo.todo.os.access", return_value=True
|
||||
):
|
||||
output = self.report(todo, 0)
|
||||
self.assertIn("✅", output)
|
||||
self.assertIn(PATH_HINT, output)
|
||||
|
||||
def test_success_without_binary_is_not_a_success(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
with patch("script.todo.todo.shutil.which", return_value=None), patch(
|
||||
"script.todo.todo.os.access", return_value=False
|
||||
):
|
||||
output = self.report(todo, 0)
|
||||
self.assertIn("❌", output)
|
||||
self.assertNotIn("✅", output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue