diff --git a/script/qemu/README.base.md b/script/qemu/README.base.md index 0ca8c1a..f99f1dd 100644 --- a/script/qemu/README.base.md +++ b/script/qemu/README.base.md @@ -263,8 +263,10 @@ download an image, list VMs and show a VM IP address — the menu asks for the parameters and builds the command for you. When a VM is graphical, the menu also offers a **check list of development -tools**: PyCharm (installed from the official JetBrains archive -into `/opt/pycharm`, its launcher opening the ERPLibre checkout), Android +tools**: PyCharm Community (installed from the official +JetBrains archive into `/opt/pycharm`, its launcher opening the ERPLibre +checkout — the Community line, because the unified 2025.3 build stops on a +licence screen and never opens a project), Android Studio (`/opt/android-studio`, command `studio` or `android-studio`; x86_64 only — Google publishes no Linux aarch64 build) and a set of suggested GNOME extensions. @@ -282,9 +284,13 @@ The tools are installed **before** the clone and the ERPLibre install, and the order matters: PyCharm writes the repository's `.idea/` the first time it opens the project, and the install that follows runs `pycharm_configuration.py` on it (`update_env_version.pycharm_update()`, -which skips silently when there is no `.idea` yet). So on a fresh VM: open -PyCharm once on `~/git/erplibre`, close it, then re-run -`make install_odoo_18`. +which skips silently when there is no `.idea` yet). That first open is automated: PyCharm runs once under +Xvfb — a virtual framebuffer inside the guest, so the orchestrating host +needs no graphics at all — with the trust, privacy and data-sharing dialogs +answered in advance. Measured on an Ubuntu 26.04 VM with 16 GB: `.idea/` is +written in 195 s, and the install then adds its exclusions to the `.iml`. +When Xvfb is unavailable or the IDE does not get there in five minutes, the +log says so and the install carries on. Each tool is filtered per VM — by architecture and by desktop flavour — and its disk cost is added to the plan before anything is created. @@ -351,8 +357,10 @@ l'IP d'une VM — le menu demande les paramètres et construit la commande pour vous. Quand une VM est graphique, le menu propose en plus une **liste à cocher -d'outils de développement** : PyCharm (posé depuis l'archive officielle -JetBrains dans `/opt/pycharm`, son lanceur ouvrant le dépôt ERPLibre), +d'outils de développement** : PyCharm Community (posé depuis l'archive +officielle JetBrains dans `/opt/pycharm`, son lanceur ouvrant le dépôt +ERPLibre — la ligne Community, car le build unifié 2025.3 s'arrête sur un +écran de licence et n'ouvre jamais de projet), Android Studio (`/opt/android-studio`, commande `studio` ou `android-studio` ; x86_64 seulement — Google ne publie aucune archive Linux aarch64) et un jeu @@ -373,8 +381,14 @@ Les outils sont posés **avant** le clone et l'installation d'ERPLibre, et l'ordre compte : PyCharm écrit le `.idea/` du dépôt à la première ouverture du projet, et l'installation qui suit y lance `pycharm_configuration.py` (`update_env_version.pycharm_update()`, qui se tait tant qu'il n'y a pas de -`.idea`). Sur une VM neuve : ouvrir PyCharm une fois sur `~/git/erplibre`, le -fermer, puis relancer `make install_odoo_18`. +`.idea`). Cette première ouverture est automatisée : PyCharm est lancé une fois sous +Xvfb — un serveur d'affichage virtuel DANS la VM invitée, si bien que l'hôte +qui orchestre n'a besoin d'aucune bibliothèque graphique — avec les fenêtres +de confiance, de confidentialité et de partage de données répondues d'avance. +Mesuré sur une VM Ubuntu 26.04 à 16 Go : le `.idea/` est écrit en 195 s, et +l'installation y ajoute ensuite ses exclusions dans le `.iml`. Sans Xvfb, ou +si l'IDE n'y arrive pas en cinq minutes, le journal le dit et l'installation +continue. Chaque outil est filtré VM par VM — architecture et saveur de bureau — et sa place disque s'ajoute au plan avant que rien ne soit créé. diff --git a/script/qemu/README.fr.md b/script/qemu/README.fr.md index 2d86503..fc9240c 100644 --- a/script/qemu/README.fr.md +++ b/script/qemu/README.fr.md @@ -147,8 +147,10 @@ l'IP d'une VM — le menu demande les paramètres et construit la commande pour vous. Quand une VM est graphique, le menu propose en plus une **liste à cocher -d'outils de développement** : PyCharm (posé depuis l'archive officielle -JetBrains dans `/opt/pycharm`, son lanceur ouvrant le dépôt ERPLibre), +d'outils de développement** : PyCharm Community (posé depuis l'archive +officielle JetBrains dans `/opt/pycharm`, son lanceur ouvrant le dépôt +ERPLibre — la ligne Community, car le build unifié 2025.3 s'arrête sur un +écran de licence et n'ouvre jamais de projet), Android Studio (`/opt/android-studio`, commande `studio` ou `android-studio` ; x86_64 seulement — Google ne publie aucune archive Linux aarch64) et un jeu @@ -169,8 +171,14 @@ Les outils sont posés **avant** le clone et l'installation d'ERPLibre, et l'ordre compte : PyCharm écrit le `.idea/` du dépôt à la première ouverture du projet, et l'installation qui suit y lance `pycharm_configuration.py` (`update_env_version.pycharm_update()`, qui se tait tant qu'il n'y a pas de -`.idea`). Sur une VM neuve : ouvrir PyCharm une fois sur `~/git/erplibre`, le -fermer, puis relancer `make install_odoo_18`. +`.idea`). Cette première ouverture est automatisée : PyCharm est lancé une fois sous +Xvfb — un serveur d'affichage virtuel DANS la VM invitée, si bien que l'hôte +qui orchestre n'a besoin d'aucune bibliothèque graphique — avec les fenêtres +de confiance, de confidentialité et de partage de données répondues d'avance. +Mesuré sur une VM Ubuntu 26.04 à 16 Go : le `.idea/` est écrit en 195 s, et +l'installation y ajoute ensuite ses exclusions dans le `.iml`. Sans Xvfb, ou +si l'IDE n'y arrive pas en cinq minutes, le journal le dit et l'installation +continue. Chaque outil est filtré VM par VM — architecture et saveur de bureau — et sa place disque s'ajoute au plan avant que rien ne soit créé. diff --git a/script/qemu/README.md b/script/qemu/README.md index 76a2407..63a5eca 100644 --- a/script/qemu/README.md +++ b/script/qemu/README.md @@ -137,8 +137,10 @@ download an image, list VMs and show a VM IP address — the menu asks for the parameters and builds the command for you. When a VM is graphical, the menu also offers a **check list of development -tools**: PyCharm (installed from the official JetBrains archive -into `/opt/pycharm`, its launcher opening the ERPLibre checkout), Android +tools**: PyCharm Community (installed from the official +JetBrains archive into `/opt/pycharm`, its launcher opening the ERPLibre +checkout — the Community line, because the unified 2025.3 build stops on a +licence screen and never opens a project), Android Studio (`/opt/android-studio`, command `studio` or `android-studio`; x86_64 only — Google publishes no Linux aarch64 build) and a set of suggested GNOME extensions. @@ -156,9 +158,13 @@ The tools are installed **before** the clone and the ERPLibre install, and the order matters: PyCharm writes the repository's `.idea/` the first time it opens the project, and the install that follows runs `pycharm_configuration.py` on it (`update_env_version.pycharm_update()`, -which skips silently when there is no `.idea` yet). So on a fresh VM: open -PyCharm once on `~/git/erplibre`, close it, then re-run -`make install_odoo_18`. +which skips silently when there is no `.idea` yet). That first open is automated: PyCharm runs once under +Xvfb — a virtual framebuffer inside the guest, so the orchestrating host +needs no graphics at all — with the trust, privacy and data-sharing dialogs +answered in advance. Measured on an Ubuntu 26.04 VM with 16 GB: `.idea/` is +written in 195 s, and the install then adds its exclusions to the `.iml`. +When Xvfb is unavailable or the IDE does not get there in five minutes, the +log says so and the install carries on. Each tool is filtered per VM — by architecture and by desktop flavour — and its disk cost is added to the plan before anything is created. diff --git a/script/todo/todo.py b/script/todo/todo.py index 748f55a..3de71fc 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -5098,10 +5098,25 @@ class TODO: # couvre les quatre gestionnaires (Arch l'a dans extra, Debian et Ubuntu ne # l'ont qu'en snap — coupé ici —, Fedora et openSUSE pas du tout). # - # L'URL ne porte AUCUN numéro de version : « code=PCC&latest » redirige vers - # la dernière stable. Vérifié, les deux : « distribution=linux » sort - # pycharm-2025.3.tar.gz et « distribution=linuxARM64 » son équivalent - # aarch64. Rien à mettre à jour dans ce dépôt quand JetBrains publie. + # La ligne COMMUNITY, et non le produit unifié. Mesuré dans une VM : + # « code=PCC&latest » sert maintenant pycharm-2025.3, le build unifié, qui + # s'arrête sur sa licence — son journal dit « NoValidIdeLicense » puis + # « Get licenses: request requires authentication », et le projet ne + # s'ouvre jamais. Aucune ouverture, donc aucun .idea, donc rien à + # configurer ensuite. Community ne demande aucun compte, et elle est + # toujours publiée et corrigée : 2025.2.6.2 date du 2026-07-29. + # + # Aucun numéro figé ici : on prend la plus récente archive + # « pycharm-community- » du flux officiel des versions, pour + # l'architecture de la VM. + _QEMU_PYCHARM_FEED = ( + "https://data.services.jetbrains.com/products/releases" + "?code=PCC&type=release" + ) + # Repli quand le flux est injoignable : la redirection « dernière version ». + # Elle sert le build unifié, donc on le DIT — l'utilisateur devra ouvrir un + # compte JetBrains, et mieux vaut l'apprendre dans le journal qu'au premier + # lancement. _QEMU_PYCHARM_URL = ( "https://download.jetbrains.com/product?code=PCC&latest&distribution=" ) @@ -5186,8 +5201,31 @@ class TODO: "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 + # architecture. En python plutôt qu'en shell : il fait la requête, + # lit le JSON et rend une ligne — sans jq, absent des images cloud. + "url=$(python3 - \"$jb\" <<'ELPYJB'\n" + "import json, sys, urllib.request\n" + "key = sys.argv[1]\n" + "try:\n" + f' with urllib.request.urlopen("{self._QEMU_PYCHARM_FEED}",\n' + " timeout=30) as fh:\n" + " data = json.load(fh)\n" + "except Exception:\n" + " sys.exit(0)\n" + 'for rel in data.get("PCC", []):\n' + ' link = (rel.get("downloads") or {}).get(key, {}).get("link", "")\n' + ' if "pycharm-community-" in link:\n' + " print(link)\n" + " break\n" + "ELPYJB\n" + "); " + 'if [ -z "$url" ]; then ' + f'url="{self._QEMU_PYCHARM_URL}$jb"; ' + f'echo " {t("release feed unreachable: unified build, it will ask for a JetBrains account")}"; ' + "fi; " "tmp=$(mktemp -p /var/tmp pycharm-XXXX.tar.gz) && " - f'curl -fsSL "{self._QEMU_PYCHARM_URL}$jb" -o "$tmp" && ' + '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; } && { ' @@ -5213,6 +5251,117 @@ class TODO: f'}} || echo " ⚠ {t("PyCharm not installed (see above)")}"; ' ) + # Serveur X virtuel, par gestionnaire de paquets. Les noms ne se + # ressemblent pas d'une famille à l'autre — relevés dans chaque dépôt, pas + # devinés. + _QEMU_XVFB_PKG = { + "apt": "xvfb", + "dnf": "xorg-x11-server-Xvfb", + "zypper": "xorg-x11-server-Xvfb", + "pacman": "xorg-server-xvfb", + } + + # Attente maximale du .idea, en tours de 5 s — cinq minutes. Mesuré sur une + # VM Ubuntu 26.04 à 16 Go : le projet est écrit en 195 s, indexation du + # dépôt en cours. On n'attend donc PAS la fin de cette indexation, qui dure + # bien plus et dont personne n'a besoin ici : pycharm_configuration.py ne + # réclame que le .iml et misc.xml. + _QEMU_PYCHARM_OPEN_TRIES = 60 + + def _qemu_xvfb_install_cmd(self): + """Pose Xvfb avec le gestionnaire de paquets présent, sans bruit.""" + x = self._QEMU_XVFB_PKG + return ( + "if command -v apt-get >/dev/null 2>&1; then " + "sudo DEBIAN_FRONTEND=noninteractive apt-get " + f"-o DPkg::Lock::Timeout=600 install -y {x['apt']} " + ">/dev/null 2>&1 || true; " + "elif command -v dnf >/dev/null 2>&1; then " + f"sudo dnf install -y {x['dnf']} >/dev/null 2>&1 || true; " + "elif command -v zypper >/dev/null 2>&1; then " + "sudo zypper --non-interactive install --auto-agree-with-licenses " + f"{x['zypper']} >/dev/null 2>&1 || true; " + "elif command -v pacman >/dev/null 2>&1; then " + f"sudo pacman -S --needed --noconfirm {x['pacman']} " + ">/dev/null 2>&1 || true; fi; " + ) + + def _qemu_pycharm_project_cmd(self, prod=False): + """Crée le .idea/ du dépôt en ouvrant PyCharm une fois, sans écran. + + C'est PyCharm, et lui seul, qui écrit ce répertoire : ni le dépôt ni + pycharm_configuration.py ne savent le fabriquer — ce dernier exige un + .iml puis un misc.xml, et s'arrête sinon. Sans cette ouverture, l'étape + pycharm_update() de l'installation ne trouve rien à configurer. + + Xvfb parce que l'IDE réclame un affichage, même pour ouvrir un projet + et s'arrêter. Il tourne DANS la VM : l'hôte qui orchestre n'a besoin + d'aucune bibliothèque graphique, et rien ne transite par « ssh -X ». + + TROIS fenêtres bloqueraient une session où personne ne peut cliquer, et + chacune a été rencontrée avant d'être écartée : politique de + confidentialité, partage de données, et surtout « faites-vous confiance + à ce projet ? ». C'est celle-là qui figeait tout — le journal s'arrêtait + 1,3 s après le démarrage, sans jamais ouvrir le projet, et il a fallu + « idea.trust.all.projects » pour le débloquer. Le consentement, lui, est + écrit REFUSÉ : aucune statistique ne part. + + Mesuré sur une VM Ubuntu 26.04 à 16 Go : .idea complet en 195 s, et + pycharm_configuration.py écrit ensuite ses exclusions dans le .iml. + + Tout est gardé. Sans Xvfb, sans PyCharm, ou sans .idea au bout du + délai, on le dit et l'installation continue : elle n'en dépend pas, + elle en profite seulement. + """ + el_dir = self._qemu_install_dir(prod) + return ( + f'echo "== {t("Creating the PyCharm project (first open)")} =="; ' + "{ if ! command -v pycharm >/dev/null 2>&1; then " + f'echo " {t("PyCharm missing, step skipped")}"; false; ' + "else " + "command -v xvfb-run >/dev/null 2>&1 || { " + + self._qemu_xvfb_install_cmd() + + "}; " + "if ! command -v xvfb-run >/dev/null 2>&1; then " + f'echo " {t("no Xvfb here, open PyCharm by hand")}"; false; ' + "else " + # Réponses aux fenêtres de première ouverture. En python plutôt + # qu'en shell : l'horodatage en millisecondes et le « » de + # la propriété se passeraient mal de guillemets imbriqués. + "python3 - <<'ELPYC' || true\n" + "import pathlib, time\n" + "h = pathlib.Path.home()\n" + 'c = h / ".local/share/JetBrains/consentOptions"\n' + "c.mkdir(parents=True, exist_ok=True)\n" + '(c / "accepted").write_text(\n' + ' "rsch.send.usage.stat:1.1:0:%d\\n" % (time.time() * 1000)\n' + ")\n" + '(h / ".pycharm-headless.vmoptions").write_text(\n' + ' "-Djb.privacy.policy.text=\\n"\n' + ' "-Djb.consents.confirmation.enabled=false\\n"\n' + ' "-Didea.trust.all.projects=true\\n"\n' + ' "-Didea.suppress.statistics.report=true\\n"\n' + ")\n" + "ELPYC\n" + 'PYCHARM_VM_OPTIONS="$HOME/.pycharm-headless.vmoptions" ' + f"xvfb-run -a pycharm {el_dir} " + "> /tmp/pycharm-first-run.log 2>&1 & " + "pid=$!; ok=0; " + f"for i in $(seq 1 {self._QEMU_PYCHARM_OPEN_TRIES}); do " + f"if ls {el_dir}/.idea/*.iml >/dev/null 2>&1 && " + f"[ -f {el_dir}/.idea/misc.xml ]; then ok=1; break; fi; " + "sleep 5; done; " + # Cinq secondes de plus : les fichiers apparaissent PENDANT leur + # écriture, et un TERM à l'instant où misc.xml naît le tronquerait. + "sleep 5; kill -TERM $pid 2>/dev/null; " + "for i in $(seq 1 12); do kill -0 $pid 2>/dev/null || break; " + "sleep 5; done; kill -KILL $pid 2>/dev/null; " + '[ "$ok" = 1 ]; fi; fi; } && ' + f'echo " {t("project created, the install will configure it")}" ' + f'|| echo " ⚠ {t("no .idea: open PyCharm once, then")} ' + 'make pycharm_configure"; ' + ) + def _qemu_android_studio_remote_cmd(self): """Installe Android Studio, pour le développement mobile ERPLibre. @@ -5543,6 +5692,14 @@ class TODO: # correctifs de sécurité automatiques doivent rester actifs. no_auto_upgrade = self._qemu_no_auto_upgrade(prod, app_store) tools_cmd = self._qemu_tools_remote_cmd(tools, prod) + # Entre le clone et le make : PyCharm ouvre le dépôt une fois pour en + # écrire le .idea, que l'installation configurera juste après. Le + # groupe rend toujours 0 — l'étape est un bonus, pas une condition. + open_step = ( + f"{{ {self._qemu_pycharm_project_cmd(prod)} }} && " + if "pycharm" in (tools or ()) + else "" + ) return ( "set -e; " + self._qemu_cloud_init_wait() # Coupé AVANT les apt-get ci-dessous : sinon apt-daily peut reprendre @@ -5652,7 +5809,7 @@ class TODO: f"sudo git clone --branch {shlex.quote(branch)} " f"{self.ERPLIBRE_GIT_URL} /opt/erplibre; " "sudo chown -R $(id -un):$(id -gn) /opt/erplibre; fi; " - f"cd /opt/erplibre && {final_cmd}" + f"cd /opt/erplibre && {open_step}{final_cmd}" ) if prod else ( @@ -5660,7 +5817,7 @@ class TODO: "if [ ! -d ~/git/erplibre/.git ]; then " f"git clone --branch {shlex.quote(branch)} " f"{self.ERPLIBRE_GIT_URL} ~/git/erplibre; fi; " - f"cd ~/git/erplibre && {final_cmd}" + f"cd ~/git/erplibre && {open_step}{final_cmd}" ) ) ) diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index bdec2d7..7ec8eee 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -7672,6 +7672,30 @@ TRANSLATIONS = { "en": "open the project once and close PyCharm; the .idea it writes is what the install configures", }, "command": {"fr": "commande", "en": "command"}, + "release feed unreachable: unified build, it will ask for a JetBrains account": { + "fr": "flux des versions injoignable : build unifié, il demandera un compte JetBrains", + "en": "release feed unreachable: unified build, it will ask for a JetBrains account", + }, + "Creating the PyCharm project (first open)": { + "fr": "Création du projet PyCharm (première ouverture)", + "en": "Creating the PyCharm project (first open)", + }, + "PyCharm missing, step skipped": { + "fr": "PyCharm absent, étape sautée", + "en": "PyCharm missing, step skipped", + }, + "no Xvfb here, open PyCharm by hand": { + "fr": "pas de Xvfb ici : ouvrir PyCharm à la main", + "en": "no Xvfb here, open PyCharm by hand", + }, + "project created, the install will configure it": { + "fr": "projet créé, l'installation va le configurer", + "en": "project created, the install will configure it", + }, + "no .idea: open PyCharm once, then": { + "fr": "pas de .idea : ouvrir PyCharm une fois, puis", + "en": "no .idea: open PyCharm once, then", + }, "project": {"fr": "projet", "en": "project"}, } diff --git a/test/test_qemu_desktop_tools.py b/test/test_qemu_desktop_tools.py index b28584c..bf37b60 100644 --- a/test/test_qemu_desktop_tools.py +++ b/test/test_qemu_desktop_tools.py @@ -224,6 +224,94 @@ class TestToolDiscoverability(unittest.TestCase): self.assertIn(".idea", cmd) +class TestPycharmCommunity(unittest.TestCase): + """PyCharm doit s'ouvrir sans compte : c'est toute la différence entre une + VM utilisable au premier démarrage et une VM qui demande une licence.""" + + def setUp(self): + self.todo = TODO.__new__(TODO) + self.cmd = self.todo._qemu_pycharm_remote_cmd() + + def test_the_community_line_is_what_is_looked_up(self): + """Mesuré dans une VM : le build unifié s'arrête sur + « NoValidIdeLicense » et n'ouvre jamais le projet.""" + self.assertIn("pycharm-community-", self.cmd) + self.assertIn("data.services.jetbrains.com", self.cmd) + + def test_no_version_is_frozen_in_the_repository(self): + """Le flux donne la plus récente : rien à mettre à jour ici quand + JetBrains publie un correctif.""" + self.assertNotIn("2025.2.6", self.cmd) + self.assertNotIn("pycharm-community-2", self.cmd) + + def test_both_architectures_are_asked_for(self): + self.assertIn("linuxARM64", self.cmd) + self.assertIn("jb=linux", self.cmd) + + def test_the_fallback_names_its_cost(self): + """Le repli sert le build unifié : le dire, plutôt que de le laisser + découvrir au premier lancement.""" + self.assertIn("code=PCC&latest", self.cmd) + self.assertIn("JetBrains", self.cmd) + + +class TestPycharmFirstOpen(unittest.TestCase): + """Ouverture sans écran, pour que le .idea existe avant l'installation.""" + + def setUp(self): + self.todo = TODO.__new__(TODO) + self.cmd = self.todo._qemu_pycharm_project_cmd() + + def test_it_runs_between_the_clone_and_the_make(self): + script = self.todo._qemu_erplibre_remote_cmd( + "develop", None, False, "gnome", "", "deb", ("pycharm",) + ) + self.assertLess(script.index("git clone"), script.index("xvfb-run")) + self.assertLess( + script.index("xvfb-run"), script.index("make install_os") + ) + + def test_only_when_pycharm_was_asked_for(self): + script = self.todo._qemu_erplibre_remote_cmd( + "develop", None, False, "gnome", "", "deb", ("android",) + ) + self.assertNotIn("xvfb-run", script) + + def test_the_first_run_dialogs_are_answered_in_advance(self): + """Sans réponse, la session attend un clic que personne ne donnera. + + Celle de la CONFIANCE est la plus coûteuse à rater : mesuré, le journal + s'arrête 1,3 s après le démarrage et le projet ne s'ouvre jamais.""" + self.assertIn("idea.trust.all.projects=true", self.cmd) + self.assertIn("jb.consents.confirmation.enabled=false", self.cmd) + self.assertIn("consentOptions", self.cmd) + # Le consentement est écrit REFUSÉ : aucune statistique ne part. + self.assertIn("rsch.send.usage.stat:1.1:0:", self.cmd) + + def test_xvfb_package_names_are_per_family(self): + """« xvfb » n'existe que chez Debian : ailleurs le paquet s'appelle + autrement, et un nom inventé ne s'installerait pas.""" + self.assertEqual("xvfb", TODO._QEMU_XVFB_PKG["apt"]) + self.assertEqual("xorg-x11-server-Xvfb", TODO._QEMU_XVFB_PKG["dnf"]) + self.assertEqual("xorg-server-xvfb", TODO._QEMU_XVFB_PKG["pacman"]) + + def test_it_gives_up_rather_than_hangs(self): + """Un IDE qui ne s'ouvre pas ne doit pas retenir l'installation : le + budget est borné et le processus tué.""" + self.assertIn("kill -TERM", self.cmd) + self.assertIn("kill -KILL", self.cmd) + self.assertIn(f"seq 1 {TODO._QEMU_PYCHARM_OPEN_TRIES}", self.cmd) + + def test_valid_shell(self): + res = subprocess.run( + ["bash", "-n"], + input="set -e\n" + self.cmd, + text=True, + capture_output=True, + ) + self.assertEqual(0, res.returncode, res.stderr) + + class TestGnomeSiteExtensions(unittest.TestCase): """Extensions posées depuis extensions.gnome.org, par leur UUID."""