diff --git a/script/todo/todo.py b/script/todo/todo.py index 2e9e709..907f39f 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -5606,6 +5606,15 @@ class TODO: 'if [ -z "$jb" ]; then ' f'echo " {t("no JetBrains build for")} $(uname -m)"; false; ' "else " + # Déjà posé ? On ne retélécharge pas. Rejouer une + # installation est le cas NORMAL — une qui est morte, un outil + # ajouté après coup — et le téléchargement en est la partie + # longue : mesuré, ~5 min pour Android Studio, autant pour + # PyCharm. Le reste de l'étape (lanceur, alias, raccourci) + # rejoue de toute façon, lui est idempotent et bon marché. + "if [ -x /opt/pycharm/bin/pycharm.sh ]; then " + f'echo " {t("already there, download skipped")}"; ' + "else " # /var/tmp et non /tmp : sur Fedora et dérivés /tmp est un tmpfs, en # RAM — 1,2 Go d'archive y tueraient une VM de 3 Go. # Le flux dit quelle archive Community prendre pour cette @@ -5635,7 +5644,7 @@ class TODO: 'curl -fsSL "$url" -o "$tmp" && ' "sudo mkdir -p /opt/pycharm && " 'sudo tar -xzf "$tmp" -C /opt/pycharm --strip-components=1; ' - 'rc=$?; rm -f "$tmp"; [ $rc -eq 0 ]; fi; } && { ' + 'rc=$?; rm -f "$tmp"; [ $rc -eq 0 ]; fi; fi; } && { ' + self._qemu_jetbrains_launcher_cmd("/opt/pycharm", "pycharm") + self._qemu_desktop_entry_cmd( "pycharm", @@ -5825,6 +5834,15 @@ class TODO: f'echo " {t("Android Studio: Google publishes x86_64 only")}"; ' "false; " "else " + # Déjà posé ? On ne retélécharge pas. Rejouer une + # installation est le cas NORMAL — une qui est morte, un outil + # ajouté après coup — et le téléchargement en est la partie + # longue : mesuré, ~5 min pour Android Studio, autant pour + # PyCharm. Le reste de l'étape (lanceur, alias, raccourci) + # rejoue de toute façon, lui est idempotent et bon marché. + "if [ -x /opt/android-studio/bin/studio ]; then " + f'echo " {t("already there, download skipped")}"; ' + "else " # La page officielle porte l'URL en clair ; le repli garde une # version connue qui répond, pour le jour où sa forme change. f"url=$(curl -fsSL --max-time 30 {self._QEMU_ANDROID_PAGE} " @@ -5836,7 +5854,7 @@ class TODO: "sudo mkdir -p /opt/android-studio && " 'sudo tar -xzf "$tmp" -C /opt/android-studio ' "--strip-components=1; " - 'rc=$?; rm -f "$tmp"; [ $rc -eq 0 ]; fi; } && { ' + 'rc=$?; rm -f "$tmp"; [ $rc -eq 0 ]; fi; fi; } && { ' + self._qemu_jetbrains_launcher_cmd( "/opt/android-studio", "studio", alias="android-studio" ) diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 06fde70..6db8b81 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2260,6 +2260,10 @@ TRANSLATIONS = { "line": {"fr": "ligne", "en": "line"}, "free space": {"fr": "libre", "en": "free"}, "silent": {"fr": "silence", "en": "silent"}, + "already there, download skipped": { + "fr": "déjà présent, téléchargement évité", + "en": "already there, download skipped", + }, "inotify watches raised for the IDE": { "fr": "watches inotify relevées pour l'IDE", "en": "inotify watches raised for the IDE", diff --git a/test/test_qemu_desktop_tools.py b/test/test_qemu_desktop_tools.py index 35974ee..2686849 100644 --- a/test/test_qemu_desktop_tools.py +++ b/test/test_qemu_desktop_tools.py @@ -773,6 +773,80 @@ class TestPycharmFirstOpen(unittest.TestCase): ) +class TestIdeInstallIsReplayable(unittest.TestCase): + """Rejouer une installation ne doit pas retélécharger 2 Go. + + C'est le cas NORMAL : une installation morte qu'on relance, un outil ajouté + après coup. Mesuré sur la VM, les deux étapes passent de ~5 min chacune à + 0,094 s au total quand /opt porte déjà l'IDE — le reste (lanceur, alias, + raccourci) rejoue quand même, il est idempotent et bon marché. + """ + + def setUp(self): + self.todo = TODO.__new__(TODO) + self.py = self.todo._qemu_pycharm_remote_cmd() + self.st = self.todo._qemu_android_studio_remote_cmd() + + def test_pycharm_checks_before_downloading(self): + self.assertIn("[ -x /opt/pycharm/bin/pycharm.sh ]", self.py) + self.assertLess( + self.py.index("/opt/pycharm/bin/pycharm.sh"), + self.py.index("curl"), + ) + + def test_android_studio_checks_before_downloading(self): + self.assertIn("[ -x /opt/android-studio/bin/studio ]", self.st) + self.assertLess( + self.st.index("/opt/android-studio/bin/studio"), + self.st.index("curl"), + ) + + def test_the_launcher_still_runs_when_the_download_is_skipped(self): + """Sauter le téléchargement ne doit pas sauter l'alias : c'est lui qui + rend « pycharm » et « android-studio » appelables.""" + # rindex : le chemin du lanceur apparaît aussi dans la garde, tout au + # début. C'est la DERNIÈRE occurrence — l'installation du lanceur — qui + # doit suivre le bloc de téléchargement. + for cmd, marker in ( + (self.py, "/usr/local/bin"), + (self.st, "/usr/local/bin"), + ): + self.assertGreater(cmd.rindex(marker), cmd.index("curl"), marker) + + def test_a_real_download_failure_still_fails(self): + """La garde ne doit pas avaler l'échec du cas où il faut télécharger. + On force l'absence d'IDE et un curl qui échoue.""" + import os + import tempfile + + with tempfile.TemporaryDirectory() as tmp: + bin_dir = pathlib.Path(tmp) / "bin" + bin_dir.mkdir() + (bin_dir / "curl").write_text("#!/bin/bash\nexit 22\n") + (bin_dir / "sudo").write_text("#!/bin/bash\nexit 0\n") + (bin_dir / "python3").write_text("#!/bin/bash\ncat >/dev/null\n") + for n in ("curl", "sudo", "python3"): + (bin_dir / n).chmod(0o755) + res = subprocess.run( + ["bash", "-c", self.st], + capture_output=True, + text=True, + env=dict(os.environ, PATH=f"{bin_dir}:/usr/bin:/bin"), + timeout=60, + ) + out = res.stdout + res.stderr + # Sur cette machine /opt/android-studio n'existe pas : la garde + # laisse donc passer, et l'échec du curl doit se voir. + self.assertIn("⚠", out, out[-300:]) + + def test_both_steps_are_valid_shell(self): + for cmd in (self.py, self.st): + res = subprocess.run( + ["bash", "-n"], input=cmd, capture_output=True, text=True + ) + self.assertEqual(0, res.returncode, res.stderr) + + class TestPycharmNetIsNarrow(unittest.TestCase): """Le filet de fermeture ne doit JAMAIS viser le ssh qui porte l'install.