[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Outils de développement des VM graphiques : filtrage, disque, commande.
|
|
|
|
|
|
|
|
|
|
Ce qui se vérifie ici sans VM : qu'un outil demandé pour tout le parc n'atterrit
|
|
|
|
|
que sur les machines qui peuvent le recevoir, que la place disque annoncée suit
|
|
|
|
|
ce filtrage, et qu'un outil qui échoue ne fait pas tomber l'installation
|
|
|
|
|
d'ERPLibre avec lui — celle-ci ayant duré une heure.
|
|
|
|
|
"""
|
|
|
|
|
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
import pathlib
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
2026-09-14 14:41:19 -04:00
|
|
|
from unittest import mock
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
|
|
|
|
|
sys.argv = ["todo.py"]
|
|
|
|
|
from script.todo.todo import TODO # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestToolFiltering(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
self.all = tuple(TODO._QEMU_VM_TOOLS)
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
|
|
|
|
|
def test_a_server_gets_no_graphical_tool(self):
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
"""Un IDE sans bureau n'a rien pour s'afficher. La compilation mobile,
|
|
|
|
|
elle, reste : elle compile, elle n'affiche pas."""
|
|
|
|
|
got = self.todo._qemu_tools_for(self.all, "amd64", "", "ubuntu")
|
|
|
|
|
for graphical in ("pycharm", "android", "gnome_ext"):
|
|
|
|
|
self.assertNotIn(graphical, got)
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
|
|
|
|
|
def test_android_studio_is_x86_64_only(self):
|
|
|
|
|
"""Google ne publie aucune archive Linux aarch64 : toutes les variantes
|
|
|
|
|
de l'URL rendent 404, et product-info.json ne déclare que Linux/amd64.
|
|
|
|
|
"""
|
|
|
|
|
self.assertIn(
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
"android",
|
|
|
|
|
self.todo._qemu_tools_for(self.all, "amd64", "gnome", "ubuntu"),
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
)
|
|
|
|
|
for arch in ("arm64", "s390x"):
|
|
|
|
|
self.assertNotIn(
|
|
|
|
|
"android",
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
self.todo._qemu_tools_for(self.all, arch, "gnome", "ubuntu"),
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
arch,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_pycharm_follows_jetbrains_two_architectures(self):
|
|
|
|
|
for arch in ("amd64", "arm64"):
|
|
|
|
|
self.assertIn(
|
|
|
|
|
"pycharm",
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
self.todo._qemu_tools_for(self.all, arch, "gnome", "ubuntu"),
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
arch,
|
|
|
|
|
)
|
|
|
|
|
self.assertNotIn(
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
"pycharm",
|
|
|
|
|
self.todo._qemu_tools_for(self.all, "s390x", "gnome", "ubuntu"),
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_gnome_extensions_only_under_gnome(self):
|
|
|
|
|
self.assertIn(
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
"gnome_ext",
|
|
|
|
|
self.todo._qemu_tools_for(self.all, "amd64", "gnome", "ubuntu"),
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
)
|
|
|
|
|
self.assertNotIn(
|
|
|
|
|
"gnome_ext",
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
self.todo._qemu_tools_for(self.all, "amd64", "cinnamon", "ubuntu"),
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_unknown_key_is_ignored(self):
|
|
|
|
|
self.assertEqual(
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
[],
|
|
|
|
|
self.todo._qemu_tools_for(("nope",), "amd64", "gnome", "ubuntu"),
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestToolDisk(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
self.all = tuple(TODO._QEMU_VM_TOOLS)
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
|
|
|
|
|
def test_disk_follows_the_filtering(self):
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
"""Une VM qui ne recevra pas un outil ne doit pas se voir gonfler son
|
|
|
|
|
disque de sa taille. En arm64 il en manque DEUX : Android Studio, que
|
|
|
|
|
Google ne publie qu'en x86_64, et la compilation mobile, qui en
|
|
|
|
|
dépend."""
|
|
|
|
|
full = self.todo._qemu_tools_disk_gb(
|
|
|
|
|
self.all, "amd64", "gnome", "ubuntu"
|
|
|
|
|
)
|
|
|
|
|
arm = self.todo._qemu_tools_disk_gb(
|
|
|
|
|
self.all, "arm64", "gnome", "ubuntu"
|
|
|
|
|
)
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
self.assertEqual(
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
full - arm,
|
|
|
|
|
TODO._QEMU_VM_TOOLS["android"]["disk_gb"]
|
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.
The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.
--- FR ---
Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.
La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.
Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
|
|
|
+ TODO._QEMU_VM_TOOLS["mobile"]["disk_gb"]
|
|
|
|
|
+ TODO._QEMU_VM_TOOLS["avd"]["disk_gb"],
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
)
|
|
|
|
|
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
def test_a_server_only_pays_for_what_it_gets(self):
|
|
|
|
|
"""Un serveur ne porte aucun IDE, donc il n'en paie pas le disque —
|
[ADD] script forgejo: installer une forge git en option cochable
Une case au déploiement, et une forge git auto-hébergée répond sur le port 3000,
git par SSH sur 2222. Le travail vit dans un script dédié, appelable seul sur
une machine existante : une seule autorité pour les deux usages.
Le binaire officiel est statique, donc le même fichier sert apt, dnf, pacman et
zypper — c'est ce qui rend l'option portable sans une branche par distribution.
Les architectures suivent l'amont (amd64, arm64, arm-6) ; la case se grise sur
s390x plutôt que de poser un binaire inexécutable. Les quatre secrets sont
écrits par le script : sans oauth2.JWT_SECRET, Forgejo tente de les persister
lui-même et boucle sur un app.ini qu'il n'a pas le droit d'écrire.
Vérifié sur une VM : somme de contrôle validée, service actif, API qui répond,
dépôt créé puis cloné par git, et relance en 1,5 s sans rien réécrire.
--- EN ---
One checkbox at deploy time, and a self-hosted git forge answers on port 3000,
git over SSH on 2222. The work lives in a dedicated script, callable on its own
for an existing machine: one authority for both uses.
The official binary is static, so the same file serves apt, dnf, pacman and
zypper — that is what makes the option portable without a branch per
distribution. Architectures follow upstream (amd64, arm64, arm-6); the checkbox
greys out on s390x rather than dropping a binary that cannot run. The script
writes all four secrets itself: without oauth2.JWT_SECRET, Forgejo tries to
persist them and loops on an app.ini it is not allowed to write.
Verified on a VM: checksum validated, service active, API answering, a repo
created then cloned over git, and a replay in 1.5 s rewriting nothing.
Assisted-by: Claude Opus 5
2026-08-19 19:41:39 -04:00
|
|
|
mais il paie bien ce qu'il reçoit : compilation mobile, émulateur et
|
|
|
|
|
forge. La somme est calculée depuis la table plutôt qu'écrite en
|
|
|
|
|
chiffre : ajouter un outil sans écran ne doit pas casser ce test, il
|
|
|
|
|
doit le suivre."""
|
|
|
|
|
expected = sum(
|
|
|
|
|
spec["disk_gb"]
|
|
|
|
|
for key, spec in TODO._QEMU_VM_TOOLS.items()
|
|
|
|
|
if not spec["needs_desktop"]
|
|
|
|
|
)
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
self.assertEqual(
|
[ADD] script forgejo: installer une forge git en option cochable
Une case au déploiement, et une forge git auto-hébergée répond sur le port 3000,
git par SSH sur 2222. Le travail vit dans un script dédié, appelable seul sur
une machine existante : une seule autorité pour les deux usages.
Le binaire officiel est statique, donc le même fichier sert apt, dnf, pacman et
zypper — c'est ce qui rend l'option portable sans une branche par distribution.
Les architectures suivent l'amont (amd64, arm64, arm-6) ; la case se grise sur
s390x plutôt que de poser un binaire inexécutable. Les quatre secrets sont
écrits par le script : sans oauth2.JWT_SECRET, Forgejo tente de les persister
lui-même et boucle sur un app.ini qu'il n'a pas le droit d'écrire.
Vérifié sur une VM : somme de contrôle validée, service actif, API qui répond,
dépôt créé puis cloné par git, et relance en 1,5 s sans rien réécrire.
--- EN ---
One checkbox at deploy time, and a self-hosted git forge answers on port 3000,
git over SSH on 2222. The work lives in a dedicated script, callable on its own
for an existing machine: one authority for both uses.
The official binary is static, so the same file serves apt, dnf, pacman and
zypper — that is what makes the option portable without a branch per
distribution. Architectures follow upstream (amd64, arm64, arm-6); the checkbox
greys out on s390x rather than dropping a binary that cannot run. The script
writes all four secrets itself: without oauth2.JWT_SECRET, Forgejo tries to
persist them and loops on an app.ini it is not allowed to write.
Verified on a VM: checksum validated, service active, API answering, a repo
created then cloned over git, and a replay in 1.5 s rewriting nothing.
Assisted-by: Claude Opus 5
2026-08-19 19:41:39 -04:00
|
|
|
expected,
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
self.todo._qemu_tools_disk_gb(self.all, "amd64", "", "ubuntu"),
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_the_deploy_command_grows_the_disk(self):
|
|
|
|
|
"""Le disque demandé à deploy_qemu.py doit inclure les outils : c'est
|
|
|
|
|
la seule valeur qui compte, celle du qcow2 réellement créé."""
|
|
|
|
|
spec = {
|
|
|
|
|
"ssh_key": "",
|
|
|
|
|
"desktop": "gnome",
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
"vm_tools": self.all,
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
"install": {
|
|
|
|
|
"branch": "develop",
|
|
|
|
|
"prod": False,
|
|
|
|
|
"cmd": "make install_os && make install_odoo_18",
|
|
|
|
|
"label": "x",
|
|
|
|
|
"monitor": True,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
vm = {
|
|
|
|
|
"distro": "ubuntu",
|
|
|
|
|
"version": "24.04",
|
|
|
|
|
"arch": "amd64",
|
|
|
|
|
"name": "v",
|
|
|
|
|
"ram": 3072,
|
|
|
|
|
"vcpus": 4,
|
|
|
|
|
"disk": "20G",
|
|
|
|
|
"desktop": "gnome",
|
|
|
|
|
}
|
|
|
|
|
parts = self.todo._qemu_deploy_parts_for(vm, spec, dry_run=True)
|
|
|
|
|
size = parts[parts.index("--disk-size") + 1]
|
|
|
|
|
expected = (
|
|
|
|
|
20
|
|
|
|
|
+ TODO.ERPLIBRE_EXTRA_DISK_GB
|
|
|
|
|
+ TODO.QEMU_DESKTOP_EXTRA_DISK_GB
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
+ sum(s["disk_gb"] for s in TODO._QEMU_VM_TOOLS.values())
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
)
|
|
|
|
|
self.assertEqual(f"{expected}G", size)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestToolRemoteCommand(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
self.all = tuple(TODO._QEMU_VM_TOOLS)
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
|
|
|
|
|
def _sh_ok(self, script):
|
|
|
|
|
"""Le shell accepte-t-il ce script ? « bash -n » ne l'exécute pas."""
|
|
|
|
|
return subprocess.run(
|
|
|
|
|
["bash", "-n"], input=script, text=True, capture_output=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_every_combination_is_valid_shell(self):
|
|
|
|
|
combos = [
|
|
|
|
|
(),
|
|
|
|
|
("pycharm",),
|
|
|
|
|
("android",),
|
|
|
|
|
("gnome_ext",),
|
|
|
|
|
self.all,
|
|
|
|
|
]
|
|
|
|
|
for tools in combos:
|
|
|
|
|
script = self.todo._qemu_erplibre_remote_cmd(
|
|
|
|
|
"develop", None, False, "gnome", "mise", "deb", tools
|
|
|
|
|
)
|
|
|
|
|
res = self._sh_ok(script)
|
|
|
|
|
self.assertEqual(0, res.returncode, f"{tools} : {res.stderr}")
|
|
|
|
|
|
|
|
|
|
def test_tools_come_before_the_clone_and_the_make(self):
|
|
|
|
|
"""L'ordre est ce qui fait marcher la configuration du projet : PyCharm
|
|
|
|
|
écrit le .idea du dépôt en l'ouvrant une fois, et c'est l'installation
|
|
|
|
|
qui, ENSUITE, y lance pycharm_configuration.py."""
|
|
|
|
|
script = self.todo._qemu_erplibre_remote_cmd(
|
|
|
|
|
"develop", None, False, "gnome", "", "deb", ("pycharm",)
|
|
|
|
|
)
|
[ADD] qemu arch : yay et bash-completion sur l'invité, guide au MOTD
Une image cloud Arch est nue : ni bash-completion, ni accès à l'AUR. Les
deux arrivent avec l'amorçage, sur la seule branche pacman. yay-bin
plutôt que yay, dont le paquet source compile Go pour le même outil ; la
construction reste sous l'utilisateur de la VM, makepkg refusant root. Le
« || true » qui ferme le bloc porte : le groupe est le dernier membre de
sa liste « || », donc set -e s'y applique et un sudo en échec emporterait
l'installation entière. Le guide de connexion n'annonce yay que si une
installation a eu lieu.
Vérifié : 10 tests, dont « bash -n » sur la commande distante entière et
la survie du bloc sous set -e avec un PATH vide.
--- EN ---
An Arch cloud image is bare: no bash-completion, no AUR access. Both come
with the bootstrap, on the pacman branch alone. yay-bin rather than yay,
whose source package compiles Go for the same tool; the build stays under
the VM user, makepkg refusing root. The « || true » closing the block
carries weight: the group is the last member of its « || » list, so set -e
applies inside it and one failing sudo would take the whole install down.
The login guide announces yay only when an install ran.
Checked: 10 tests, among them « bash -n » over the whole remote command
and the block surviving set -e with an empty PATH.
Assisted-by: Claude Opus 5
2026-09-02 08:04:05 -04:00
|
|
|
# « git clone --branch » et non « git clone » : l'amorçage Arch en
|
|
|
|
|
# pose un autre, celui de yay-bin, et il arrive plus tôt. Seul le
|
|
|
|
|
# clone du dépôt, qui seul porte une branche, est en jeu ici.
|
|
|
|
|
self.assertLess(
|
|
|
|
|
script.index("PyCharm"), script.index("git clone --branch")
|
|
|
|
|
)
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
self.assertLess(
|
|
|
|
|
script.index("PyCharm"), script.index("make install_os")
|
|
|
|
|
)
|
|
|
|
|
|
[FIX] script todo: configurer le projet PyCharm avec le venv, et réessayer
Deux défauts empêchaient le .idea d'exister. La configuration d'abord :
« make pycharm_configure » lance le script avec le python SYSTÈME, qui n'a pas
xmltodict — « ModuleNotFoundError », mesuré. update_env_version.pycharm_update()
l'appelle depuis .venv.erplibre ; la cible make et l'étape font désormais pareil.
L'ouverture ensuite : la première tentative sur un dépôt neuf peut n'écrire
aucun .idea, son configurateur d'interpréteur plantant sur « homeDir is null »,
là où la suivante l'écrit en 25 s — constaté sur deux VM. L'étape retente donc
une fois, en gardant les deux journaux. Vérifié sur erplibre-ubuntu-2604-gnome,
caches effacés : erplibre.iml, misc.xml, modules.xml, vcs.xml, et 0 processus
survivant.
--- EN ---
Two defects kept .idea from existing. The configuration first: "make
pycharm_configure" runs the script with the SYSTEM python, which lacks
xmltodict — "ModuleNotFoundError", measured. update_env_version.pycharm_update()
calls it from .venv.erplibre; the make target and the step now do the same.
The open next: the first attempt on a fresh repo can write no .idea at all, its
interpreter configurator dying on "homeDir is null", where the next one writes
it in 25 s — seen on two VMs. The step therefore retries once, keeping both
logs. Verified on erplibre-ubuntu-2604-gnome with caches wiped: erplibre.iml,
misc.xml, modules.xml, vcs.xml, and 0 surviving processes.
Assisted-by: Claude Opus 5
2026-08-19 08:11:12 -04:00
|
|
|
def test_the_configuration_is_asked_for_because_the_install_was_too_early(
|
|
|
|
|
self,
|
|
|
|
|
):
|
|
|
|
|
"""Le contraire de ce que ce test exigeait avant, et pour une raison
|
|
|
|
|
mesurée : pycharm_update() teste « os.path.exists('.idea') » et se tait
|
|
|
|
|
quand le projet n'existe pas encore. Or il s'exécute PENDANT
|
|
|
|
|
l'installation, alors que PyCharm ne s'ouvrira qu'après. Personne ne
|
|
|
|
|
configurait donc le projet — « Missing ./.idea path » dans le journal
|
|
|
|
|
d'une VM neuve. On le demande maintenant explicitement, après
|
|
|
|
|
l'ouverture."""
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
script = self.todo._qemu_erplibre_remote_cmd(
|
|
|
|
|
"develop", None, False, "gnome", "", "deb", ("pycharm",)
|
|
|
|
|
)
|
[FIX] script todo: configurer le projet PyCharm avec le venv, et réessayer
Deux défauts empêchaient le .idea d'exister. La configuration d'abord :
« make pycharm_configure » lance le script avec le python SYSTÈME, qui n'a pas
xmltodict — « ModuleNotFoundError », mesuré. update_env_version.pycharm_update()
l'appelle depuis .venv.erplibre ; la cible make et l'étape font désormais pareil.
L'ouverture ensuite : la première tentative sur un dépôt neuf peut n'écrire
aucun .idea, son configurateur d'interpréteur plantant sur « homeDir is null »,
là où la suivante l'écrit en 25 s — constaté sur deux VM. L'étape retente donc
une fois, en gardant les deux journaux. Vérifié sur erplibre-ubuntu-2604-gnome,
caches effacés : erplibre.iml, misc.xml, modules.xml, vcs.xml, et 0 processus
survivant.
--- EN ---
Two defects kept .idea from existing. The configuration first: "make
pycharm_configure" runs the script with the SYSTEM python, which lacks
xmltodict — "ModuleNotFoundError", measured. update_env_version.pycharm_update()
calls it from .venv.erplibre; the make target and the step now do the same.
The open next: the first attempt on a fresh repo can write no .idea at all, its
interpreter configurator dying on "homeDir is null", where the next one writes
it in 25 s — seen on two VMs. The step therefore retries once, keeping both
logs. Verified on erplibre-ubuntu-2604-gnome with caches wiped: erplibre.iml,
misc.xml, modules.xml, vcs.xml, and 0 surviving processes.
Assisted-by: Claude Opus 5
2026-08-19 08:11:12 -04:00
|
|
|
self.assertIn("pycharm_configuration.py --init", script)
|
|
|
|
|
self.assertLess(
|
|
|
|
|
script.index("xvfb-run"),
|
|
|
|
|
script.index("pycharm_configuration.py"),
|
|
|
|
|
)
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
|
|
|
|
|
def test_a_failing_tool_never_masks_a_failing_install(self):
|
|
|
|
|
"""Le code de sortie doit rester celui de l'installation : c'est lui que
|
|
|
|
|
lit le tableau de bord pour dire ✅ ou ❌."""
|
|
|
|
|
script = (
|
|
|
|
|
"set -e\n"
|
|
|
|
|
"curl() { return 7; }; sudo() { return 7; }\n"
|
|
|
|
|
+ self.todo._qemu_tools_remote_cmd(("pycharm", "android"), False)
|
|
|
|
|
+ "\nexit 3\n" # l'installation qui suit, en échec
|
|
|
|
|
)
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-c", script], capture_output=True, text=True
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(3, res.returncode, res.stdout + res.stderr)
|
|
|
|
|
|
|
|
|
|
def test_a_failing_tool_still_returns_zero(self):
|
|
|
|
|
"""Le bloc d'outils est lui-même gardé : PyCharm indisponible ne doit
|
|
|
|
|
pas transformer une installation réussie en échec."""
|
|
|
|
|
script = "set -e\n" + self.todo._qemu_tools_remote_cmd(self.all, False)
|
|
|
|
|
# Tout ce qui pourrait réussir est neutralisé : ni réseau, ni sudo.
|
|
|
|
|
stub = (
|
|
|
|
|
"curl() { return 7; }; sudo() { return 7; }; tar() { return 7; }; "
|
|
|
|
|
"export -f curl sudo tar 2>/dev/null || true\n"
|
|
|
|
|
)
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-c", stub + script], capture_output=True, text=True
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stderr)
|
|
|
|
|
self.assertIn("PyCharm", res.stdout)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestToolDiscoverability(unittest.TestCase):
|
|
|
|
|
"""Ce qui est installé doit pouvoir être TROUVÉ. Vécu : Android Studio
|
|
|
|
|
posé dans /opt, lanceur nommé « studio », et l'utilisateur conclut à un
|
|
|
|
|
échec parce que « android-studio » ne répond pas."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
|
|
|
|
|
def test_android_studio_answers_to_both_names(self):
|
|
|
|
|
cmd = self.todo._qemu_android_studio_remote_cmd()
|
|
|
|
|
self.assertIn("/usr/local/bin/studio;", cmd)
|
|
|
|
|
self.assertIn("/usr/local/bin/android-studio;", cmd)
|
|
|
|
|
|
2026-08-18 00:51:50 -04:00
|
|
|
def test_the_sdk_is_shared_with_the_desktop_session(self):
|
|
|
|
|
"""install-android.sh écrit ses exports dans ~/.bashrc, que GNOME ne
|
|
|
|
|
lit pas : sans environment.d, Android Studio lancé depuis le menu
|
|
|
|
|
proposerait de télécharger un SECOND SDK."""
|
|
|
|
|
cmd = self.todo._qemu_android_studio_remote_cmd()
|
|
|
|
|
self.assertIn(".config/environment.d", cmd)
|
|
|
|
|
self.assertIn("ANDROID_HOME=", cmd)
|
|
|
|
|
# Le même emplacement que celui où la compilation mobile l'installe.
|
|
|
|
|
self.assertIn("/android", cmd)
|
|
|
|
|
self.assertIn("ANDROID_HOME", self.todo._qemu_mobile_remote_cmd())
|
|
|
|
|
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
def test_the_log_says_where_it_landed(self):
|
|
|
|
|
pycharm = self.todo._qemu_pycharm_remote_cmd()
|
|
|
|
|
android = self.todo._qemu_android_studio_remote_cmd()
|
|
|
|
|
self.assertIn("/opt/pycharm", pycharm)
|
|
|
|
|
self.assertIn("/opt/android-studio", android)
|
|
|
|
|
|
|
|
|
|
def test_pycharm_says_what_creates_the_project(self):
|
|
|
|
|
"""Le .idea n'existe qu'après une première ouverture de PyCharm : le
|
|
|
|
|
journal le dit, plutôt que de laisser croire à un échec."""
|
|
|
|
|
cmd = self.todo._qemu_pycharm_remote_cmd()
|
|
|
|
|
self.assertIn(".idea", cmd)
|
|
|
|
|
|
|
|
|
|
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
class TestMobileBuild(unittest.TestCase):
|
|
|
|
|
"""Compilation ERPLibre mobile : la seule étape qui peut faire échouer la
|
|
|
|
|
VM, et la seule qui n'exige pas de bureau."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.all = tuple(TODO._QEMU_VM_TOOLS)
|
|
|
|
|
|
|
|
|
|
def test_it_runs_on_a_server_vm(self):
|
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.
The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.
--- FR ---
Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.
La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.
Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
|
|
|
"""Elle compile, elle n'affiche rien : un bureau serait du gaspillage.
|
[ADD] qemu deploy : poser rtk, starship et un agent dans la VM
Une case de plus au catalogue des outils, donc une case dans les deux
écrans sans y toucher. Cochée, elle découvre le choix de l'agent — Claude
Code ou opencode — et l'identité git, pré-remplie avec celle de l'hôte :
c'est ce que la VM reçoit déjà, et un champ vide la ferait croire absente.
Ce qui est saisi prime, champ par champ.
Phase « before », où chaque outil se garde lui-même. Chaque pose est aussi
privée d'entrée standard et bornée dans le temps : « || true » couvre
l'échec, pas l'ATTENTE, et un installateur amont qui pose une question
resterait pendu sur un SSH sans terminal — d'où « -y » pour starship.
--- EN ---
One more entry in the tool catalogue, hence one more box on both screens
for free. Ticked, it reveals the agent choice — Claude Code or opencode —
and the git identity, prefilled from the host: that is what the VM
already gets, and an empty field would suggest none. What is typed wins,
field by field.
Phase « before », where each tool guards itself. Every install is also
denied stdin and time-bounded: « || true » covers failure, not WAITING,
and an upstream installer asking a question would hang on a terminal-less
SSH — hence « -y » for starship.
Assisted-by: Claude Opus 5
2026-09-03 04:12:14 -04:00
|
|
|
L'émulateur non plus n'en a pas besoin — il s'affiche par ssh -X, et
|
2026-09-16 00:41:43 -04:00
|
|
|
les outils d'assistance encore moins : on s'en sert en SSH. nix et
|
|
|
|
|
nixos-anywhere non plus : ils installent NixOS sur une AUTRE machine,
|
|
|
|
|
depuis une ligne de commande."""
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
got = self.todo._qemu_tools_for(self.all, "amd64", "", "ubuntu")
|
2026-09-16 00:41:43 -04:00
|
|
|
self.assertEqual(
|
|
|
|
|
["mobile", "forgejo", "aidev", "nixanywhere", "avd"], got
|
|
|
|
|
)
|
[ADD] script forgejo: installer une forge git en option cochable
Une case au déploiement, et une forge git auto-hébergée répond sur le port 3000,
git par SSH sur 2222. Le travail vit dans un script dédié, appelable seul sur
une machine existante : une seule autorité pour les deux usages.
Le binaire officiel est statique, donc le même fichier sert apt, dnf, pacman et
zypper — c'est ce qui rend l'option portable sans une branche par distribution.
Les architectures suivent l'amont (amd64, arm64, arm-6) ; la case se grise sur
s390x plutôt que de poser un binaire inexécutable. Les quatre secrets sont
écrits par le script : sans oauth2.JWT_SECRET, Forgejo tente de les persister
lui-même et boucle sur un app.ini qu'il n'a pas le droit d'écrire.
Vérifié sur une VM : somme de contrôle validée, service actif, API qui répond,
dépôt créé puis cloné par git, et relance en 1,5 s sans rien réécrire.
--- EN ---
One checkbox at deploy time, and a self-hosted git forge answers on port 3000,
git over SSH on 2222. The work lives in a dedicated script, callable on its own
for an existing machine: one authority for both uses.
The official binary is static, so the same file serves apt, dnf, pacman and
zypper — that is what makes the option portable without a branch per
distribution. Architectures follow upstream (amd64, arm64, arm-6); the checkbox
greys out on s390x rather than dropping a binary that cannot run. The script
writes all four secrets itself: without oauth2.JWT_SECRET, Forgejo tries to
persist them and loops on an app.ini it is not allowed to write.
Verified on a VM: checksum validated, service active, API answering, a repo
created then cloned over git, and a replay in 1.5 s rewriting nothing.
Assisted-by: Claude Opus 5
2026-08-19 19:41:39 -04:00
|
|
|
# Forgejo est là pour la même raison que la compilation : un
|
|
|
|
|
# service ne demande pas d'écran.
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
|
|
|
|
|
def test_it_is_bounded_to_apt(self):
|
|
|
|
|
"""install-android.sh du dépôt mobile commence par « sudo apt install
|
|
|
|
|
openjdk-17-jdk » : ailleurs il s'arrête là."""
|
|
|
|
|
for distro in ("fedora", "rocky", "opensuse", "arch"):
|
|
|
|
|
self.assertNotIn(
|
|
|
|
|
"mobile",
|
|
|
|
|
self.todo._qemu_tools_for(self.all, "amd64", "gnome", distro),
|
|
|
|
|
distro,
|
|
|
|
|
)
|
|
|
|
|
self.assertIn(
|
|
|
|
|
"mobile",
|
|
|
|
|
self.todo._qemu_tools_for(self.all, "amd64", "gnome", "debian"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_it_coexists_with_android_studio(self):
|
|
|
|
|
"""Combinaison croisée : la VM graphique reçoit les deux, et un seul
|
|
|
|
|
SDK — celui de $HOME/android, que ANDROID_HOME désigne."""
|
|
|
|
|
got = self.todo._qemu_tools_for(self.all, "amd64", "gnome", "ubuntu")
|
|
|
|
|
self.assertIn("android", got)
|
|
|
|
|
self.assertIn("mobile", got)
|
|
|
|
|
self.assertIn("ANDROID_HOME", self.todo._qemu_mobile_remote_cmd())
|
|
|
|
|
|
|
|
|
|
def test_it_runs_after_the_install_not_before(self):
|
|
|
|
|
"""Elle a besoin du dépôt, du venv qui synchronise le manifeste, et de
|
|
|
|
|
node que « make install_os » installe."""
|
|
|
|
|
script = self.todo._qemu_erplibre_remote_cmd(
|
|
|
|
|
"develop", None, False, "", "", "deb", ("mobile",)
|
|
|
|
|
)
|
|
|
|
|
self.assertLess(
|
|
|
|
|
script.index("make install_os"),
|
|
|
|
|
script.index("erplibre-mobile-build.log"),
|
|
|
|
|
)
|
|
|
|
|
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
# Banc d'essai des étapes mobiles : « mstep » est remplacé par une fonction
|
|
|
|
|
# qui réussit tout sauf l'étape nommée, et « sudo » par un no-op. Le contrat
|
|
|
|
|
# se MESURE alors au code de sortie, au lieu de se déduire de la présence
|
|
|
|
|
# ou de l'absence d'un « || » dans le texte — un « || echo » légitime, celui
|
|
|
|
|
# qui ajoute la ligne du fichier d'échange à /etc/fstab, faisait tomber
|
|
|
|
|
# l'ancienne version de ce test sans que rien ne soit cassé.
|
|
|
|
|
HARNESS = (
|
|
|
|
|
'mstep() { echo "-> $1"; case "$1" in *%s*) return 1;; esac; '
|
|
|
|
|
"return 0; }\n"
|
|
|
|
|
"sudo() { return 0; }\n"
|
|
|
|
|
)
|
|
|
|
|
|
[FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.
Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.
--- EN ---
The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.
Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.
Assisted-by: Claude Opus 5
2026-08-20 03:51:14 -04:00
|
|
|
def _run_steps(self, fail_on="RIEN", apk=False, transfer_ok=True):
|
|
|
|
|
"""Joue les étapes mobiles avec un « mstep » et un vérificateur de
|
|
|
|
|
transfert bouchonnés. Le vérificateur est un VRAI fichier dans l'arbre
|
|
|
|
|
d'essai : c'est ainsi qu'on éprouve le chaînage, code de sortie
|
|
|
|
|
compris."""
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
el = pathlib.Path(tmp) / "el"
|
|
|
|
|
apk_dir = el / "mobile/erplibre_home_mobile/android/app/build"
|
|
|
|
|
(apk_dir / "outputs/apk/debug").mkdir(parents=True)
|
|
|
|
|
if apk:
|
|
|
|
|
(apk_dir / "outputs/apk/debug/app-debug.apk").write_text("x")
|
[FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.
Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.
--- EN ---
The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.
Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.
Assisted-by: Claude Opus 5
2026-08-20 03:51:14 -04:00
|
|
|
checker = el / "script/mobile/check_bundle_transfer.py"
|
|
|
|
|
checker.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
checker.write_text(
|
|
|
|
|
"#!/bin/bash\necho ' 139 depots'\n"
|
|
|
|
|
+ ("exit 0\n" if transfer_ok else "exit 1\n")
|
|
|
|
|
)
|
|
|
|
|
checker.chmod(0o755)
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
steps = self.todo._qemu_mobile_build_steps(str(el))
|
|
|
|
|
return subprocess.run(
|
|
|
|
|
["bash", "-c", (self.HARNESS % fail_on) + steps],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
env=dict(os.environ, HOME=tmp),
|
|
|
|
|
timeout=60,
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
def test_a_failed_build_fails_the_vm(self):
|
|
|
|
|
"""Contrat explicite : « pour que ce soit bon », l'app doit compiler.
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
Une étape en échec doit donc remonter un code non nul."""
|
|
|
|
|
res = self._run_steps(fail_on="gradle")
|
|
|
|
|
self.assertNotEqual(0, res.returncode, res.stdout[-400:])
|
|
|
|
|
|
[FIX] script todo: ne plus empaqueter les dépôts du manifeste dans l'APK
La compilation butait sur « Too many zip entries 123678 (MAX=65535) » : un APK
est un ZIP, et le dépôt mobile verse 122 684 fichiers d'assets pour 337 qui
sont l'application. Le levier existe et il est documenté chez lui
(doc/SERVICES.md) : ERPLIBRE_MANIFEST_PATH, ici pointé sur un manifeste vide —
« ces dépôts-là : aucun ». Le plugin l'annonce, « 0 repos ».
Mesuré sur erplibre-ubuntu-2604-gnome : dist passe de 123 019 fichiers à 336,
l'APK sort à 59 Mo et 2 472 entrées, et la phase mobile entière rend 0, tests
Vitest compris — 75 fichiers, 1938 tests. Qui veut les dépôts pose la variable
lui-même : elle est respectée. Mesure d'attente, à retirer quand ils tiendront
sous le plafond du ZIP.
--- EN ---
The build hit "Too many zip entries 123678 (MAX=65535)": an APK is a ZIP, and
the mobile repo pours 122,684 asset files in for 337 that are the application.
The lever exists and that repo documents it (doc/SERVICES.md):
ERPLIBRE_MANIFEST_PATH, pointed here at an empty manifest — "those repos:
none". The plugin says so itself, "0 repos".
Measured on erplibre-ubuntu-2604-gnome: dist drops from 123,019 files to 336,
the APK comes out at 59 MB with 2,472 entries, and the whole mobile phase
returns 0, Vitest included — 75 files, 1938 tests. Set the variable yourself
and the repos come back. A stopgap, to drop once they fit under the ZIP
ceiling.
Assisted-by: Claude Opus 5
2026-08-19 05:57:22 -04:00
|
|
|
def test_a_failed_step_stops_the_ones_after_it(self):
|
|
|
|
|
"""La chaîne est en « && » d'un bout à l'autre. Un « ; » glissé au
|
|
|
|
|
milieu — celui qui écrivait le manifeste vide — laissait la compilation
|
|
|
|
|
web démarrer alors que « npm ci » venait d'échouer."""
|
|
|
|
|
res = self._run_steps(fail_on="npm")
|
|
|
|
|
self.assertNotEqual(0, res.returncode)
|
|
|
|
|
self.assertNotIn("vite build", res.stdout)
|
|
|
|
|
self.assertNotIn("gradle", res.stdout)
|
|
|
|
|
|
[FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.
Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.
--- EN ---
The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.
Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.
Assisted-by: Claude Opus 5
2026-08-20 03:51:14 -04:00
|
|
|
def test_the_manifest_repos_are_bundled_again(self):
|
|
|
|
|
"""Le contournement a vécu : les dépôts entrent maintenant en PACKS, et
|
|
|
|
|
rien ne neutralise plus le manifeste. Mesuré sur la VM : 139 dépôts,
|
|
|
|
|
116 156 fichiers en 391 tranches, 3 002 entrées dans l'APK — là où un
|
|
|
|
|
fichier par source en demandait 123 678 pour une limite de 65 535."""
|
[FIX] script todo: ne plus empaqueter les dépôts du manifeste dans l'APK
La compilation butait sur « Too many zip entries 123678 (MAX=65535) » : un APK
est un ZIP, et le dépôt mobile verse 122 684 fichiers d'assets pour 337 qui
sont l'application. Le levier existe et il est documenté chez lui
(doc/SERVICES.md) : ERPLIBRE_MANIFEST_PATH, ici pointé sur un manifeste vide —
« ces dépôts-là : aucun ». Le plugin l'annonce, « 0 repos ».
Mesuré sur erplibre-ubuntu-2604-gnome : dist passe de 123 019 fichiers à 336,
l'APK sort à 59 Mo et 2 472 entrées, et la phase mobile entière rend 0, tests
Vitest compris — 75 fichiers, 1938 tests. Qui veut les dépôts pose la variable
lui-même : elle est respectée. Mesure d'attente, à retirer quand ils tiendront
sous le plafond du ZIP.
--- EN ---
The build hit "Too many zip entries 123678 (MAX=65535)": an APK is a ZIP, and
the mobile repo pours 122,684 asset files in for 337 that are the application.
The lever exists and that repo documents it (doc/SERVICES.md):
ERPLIBRE_MANIFEST_PATH, pointed here at an empty manifest — "those repos:
none". The plugin says so itself, "0 repos".
Measured on erplibre-ubuntu-2604-gnome: dist drops from 123,019 files to 336,
the APK comes out at 59 MB with 2,472 entries, and the whole mobile phase
returns 0, Vitest included — 75 files, 1938 tests. Set the variable yourself
and the repos come back. A stopgap, to drop once they fit under the ZIP
ceiling.
Assisted-by: Claude Opus 5
2026-08-19 05:57:22 -04:00
|
|
|
steps = self.todo._qemu_mobile_build_steps("/tmp/el")
|
[FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.
Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.
--- EN ---
The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.
Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.
Assisted-by: Claude Opus 5
2026-08-20 03:51:14 -04:00
|
|
|
self.assertNotIn("ERPLIBRE_MANIFEST_PATH", steps)
|
|
|
|
|
self.assertNotIn("empty-manifest", steps)
|
[FIX] script todo: ne plus empaqueter les dépôts du manifeste dans l'APK
La compilation butait sur « Too many zip entries 123678 (MAX=65535) » : un APK
est un ZIP, et le dépôt mobile verse 122 684 fichiers d'assets pour 337 qui
sont l'application. Le levier existe et il est documenté chez lui
(doc/SERVICES.md) : ERPLIBRE_MANIFEST_PATH, ici pointé sur un manifeste vide —
« ces dépôts-là : aucun ». Le plugin l'annonce, « 0 repos ».
Mesuré sur erplibre-ubuntu-2604-gnome : dist passe de 123 019 fichiers à 336,
l'APK sort à 59 Mo et 2 472 entrées, et la phase mobile entière rend 0, tests
Vitest compris — 75 fichiers, 1938 tests. Qui veut les dépôts pose la variable
lui-même : elle est respectée. Mesure d'attente, à retirer quand ils tiendront
sous le plafond du ZIP.
--- EN ---
The build hit "Too many zip entries 123678 (MAX=65535)": an APK is a ZIP, and
the mobile repo pours 122,684 asset files in for 337 that are the application.
The lever exists and that repo documents it (doc/SERVICES.md):
ERPLIBRE_MANIFEST_PATH, pointed here at an empty manifest — "those repos:
none". The plugin says so itself, "0 repos".
Measured on erplibre-ubuntu-2604-gnome: dist drops from 123,019 files to 336,
the APK comes out at 59 MB with 2,472 entries, and the whole mobile phase
returns 0, Vitest included — 75 files, 1938 tests. Set the variable yourself
and the repos come back. A stopgap, to drop once they fit under the ZIP
ceiling.
Assisted-by: Claude Opus 5
2026-08-19 05:57:22 -04:00
|
|
|
|
[FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.
Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.
--- EN ---
The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.
Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.
Assisted-by: Claude Opus 5
2026-08-20 03:51:14 -04:00
|
|
|
def test_the_transfer_is_verified_after_the_bundle(self):
|
|
|
|
|
"""Une application qui ne porte pas le code qu'elle est censée montrer
|
|
|
|
|
n'est pas l'application demandée : le transfert se vérifie."""
|
[FIX] script todo: ne plus empaqueter les dépôts du manifeste dans l'APK
La compilation butait sur « Too many zip entries 123678 (MAX=65535) » : un APK
est un ZIP, et le dépôt mobile verse 122 684 fichiers d'assets pour 337 qui
sont l'application. Le levier existe et il est documenté chez lui
(doc/SERVICES.md) : ERPLIBRE_MANIFEST_PATH, ici pointé sur un manifeste vide —
« ces dépôts-là : aucun ». Le plugin l'annonce, « 0 repos ».
Mesuré sur erplibre-ubuntu-2604-gnome : dist passe de 123 019 fichiers à 336,
l'APK sort à 59 Mo et 2 472 entrées, et la phase mobile entière rend 0, tests
Vitest compris — 75 fichiers, 1938 tests. Qui veut les dépôts pose la variable
lui-même : elle est respectée. Mesure d'attente, à retirer quand ils tiendront
sous le plafond du ZIP.
--- EN ---
The build hit "Too many zip entries 123678 (MAX=65535)": an APK is a ZIP, and
the mobile repo pours 122,684 asset files in for 337 that are the application.
The lever exists and that repo documents it (doc/SERVICES.md):
ERPLIBRE_MANIFEST_PATH, pointed here at an empty manifest — "those repos:
none". The plugin says so itself, "0 repos".
Measured on erplibre-ubuntu-2604-gnome: dist drops from 123,019 files to 336,
the APK comes out at 59 MB with 2,472 entries, and the whole mobile phase
returns 0, Vitest included — 75 files, 1938 tests. Set the variable yourself
and the repos come back. A stopgap, to drop once they fit under the ZIP
ceiling.
Assisted-by: Claude Opus 5
2026-08-19 05:57:22 -04:00
|
|
|
steps = self.todo._qemu_mobile_build_steps("/tmp/el")
|
[FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.
Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.
--- EN ---
The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.
Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.
Assisted-by: Claude Opus 5
2026-08-20 03:51:14 -04:00
|
|
|
self.assertIn("check_bundle_transfer.py", steps)
|
[FIX] script todo: ne plus empaqueter les dépôts du manifeste dans l'APK
La compilation butait sur « Too many zip entries 123678 (MAX=65535) » : un APK
est un ZIP, et le dépôt mobile verse 122 684 fichiers d'assets pour 337 qui
sont l'application. Le levier existe et il est documenté chez lui
(doc/SERVICES.md) : ERPLIBRE_MANIFEST_PATH, ici pointé sur un manifeste vide —
« ces dépôts-là : aucun ». Le plugin l'annonce, « 0 repos ».
Mesuré sur erplibre-ubuntu-2604-gnome : dist passe de 123 019 fichiers à 336,
l'APK sort à 59 Mo et 2 472 entrées, et la phase mobile entière rend 0, tests
Vitest compris — 75 fichiers, 1938 tests. Qui veut les dépôts pose la variable
lui-même : elle est respectée. Mesure d'attente, à retirer quand ils tiendront
sous le plafond du ZIP.
--- EN ---
The build hit "Too many zip entries 123678 (MAX=65535)": an APK is a ZIP, and
the mobile repo pours 122,684 asset files in for 337 that are the application.
The lever exists and that repo documents it (doc/SERVICES.md):
ERPLIBRE_MANIFEST_PATH, pointed here at an empty manifest — "those repos:
none". The plugin says so itself, "0 repos".
Measured on erplibre-ubuntu-2604-gnome: dist drops from 123,019 files to 336,
the APK comes out at 59 MB with 2,472 entries, and the whole mobile phase
returns 0, Vitest included — 75 files, 1938 tests. Set the variable yourself
and the repos come back. A stopgap, to drop once they fit under the ZIP
ceiling.
Assisted-by: Claude Opus 5
2026-08-19 05:57:22 -04:00
|
|
|
self.assertLess(
|
|
|
|
|
steps.index("npm run build"),
|
[FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.
Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.
--- EN ---
The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.
Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.
Assisted-by: Claude Opus 5
2026-08-20 03:51:14 -04:00
|
|
|
steps.index("check_bundle_transfer.py"),
|
|
|
|
|
)
|
|
|
|
|
self.assertLess(
|
|
|
|
|
steps.index("check_bundle_transfer.py"),
|
|
|
|
|
steps.index("cap sync"),
|
[FIX] script todo: ne plus empaqueter les dépôts du manifeste dans l'APK
La compilation butait sur « Too many zip entries 123678 (MAX=65535) » : un APK
est un ZIP, et le dépôt mobile verse 122 684 fichiers d'assets pour 337 qui
sont l'application. Le levier existe et il est documenté chez lui
(doc/SERVICES.md) : ERPLIBRE_MANIFEST_PATH, ici pointé sur un manifeste vide —
« ces dépôts-là : aucun ». Le plugin l'annonce, « 0 repos ».
Mesuré sur erplibre-ubuntu-2604-gnome : dist passe de 123 019 fichiers à 336,
l'APK sort à 59 Mo et 2 472 entrées, et la phase mobile entière rend 0, tests
Vitest compris — 75 fichiers, 1938 tests. Qui veut les dépôts pose la variable
lui-même : elle est respectée. Mesure d'attente, à retirer quand ils tiendront
sous le plafond du ZIP.
--- EN ---
The build hit "Too many zip entries 123678 (MAX=65535)": an APK is a ZIP, and
the mobile repo pours 122,684 asset files in for 337 that are the application.
The lever exists and that repo documents it (doc/SERVICES.md):
ERPLIBRE_MANIFEST_PATH, pointed here at an empty manifest — "those repos:
none". The plugin says so itself, "0 repos".
Measured on erplibre-ubuntu-2604-gnome: dist drops from 123,019 files to 336,
the APK comes out at 59 MB with 2,472 entries, and the whole mobile phase
returns 0, Vitest included — 75 files, 1938 tests. Set the variable yourself
and the repos come back. A stopgap, to drop once they fit under the ZIP
ceiling.
Assisted-by: Claude Opus 5
2026-08-19 05:57:22 -04:00
|
|
|
)
|
|
|
|
|
|
[FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.
Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.
--- EN ---
The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.
Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.
Assisted-by: Claude Opus 5
2026-08-20 03:51:14 -04:00
|
|
|
def test_the_transfer_is_compared_to_the_source(self):
|
|
|
|
|
"""« --workspace » : c'est la comparaison octet pour octet qui prouve un
|
|
|
|
|
transfert FIDÈLE, et pas seulement cohérent."""
|
|
|
|
|
steps = self.todo._qemu_mobile_build_steps("/tmp/el")
|
|
|
|
|
self.assertIn("--workspace /tmp/el", steps)
|
|
|
|
|
|
|
|
|
|
def test_a_failed_transfer_fails_the_vm(self):
|
|
|
|
|
"""Une application qui ne porte pas le code qu'elle doit montrer n'est
|
|
|
|
|
pas l'application demandée. Mesuré au code de sortie, et non à la
|
|
|
|
|
présence d'un « && » dans le texte."""
|
|
|
|
|
res = self._run_steps(apk=True, transfer_ok=False)
|
|
|
|
|
self.assertNotEqual(0, res.returncode, res.stdout[-300:])
|
|
|
|
|
self.assertNotIn("gradle", res.stdout)
|
|
|
|
|
|
|
|
|
|
def test_a_good_transfer_lets_the_build_go_on(self):
|
|
|
|
|
res = self._run_steps(apk=True, transfer_ok=True)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stdout[-300:])
|
|
|
|
|
self.assertIn("139 depots", res.stdout)
|
|
|
|
|
|
|
|
|
|
def test_the_transfer_line_is_read_in_the_install_log(self):
|
|
|
|
|
"""Hors mstep, à dessein : mstep renvoie la sortie dans le journal
|
|
|
|
|
détaillé de la VM, et c'est le compte des dépôts qu'on veut voir dans
|
|
|
|
|
celui de l'installation. Le bouchon imprime une ligne : elle doit
|
|
|
|
|
remonter jusqu'à la sortie."""
|
|
|
|
|
res = self._run_steps(apk=True)
|
|
|
|
|
self.assertIn("139 depots", res.stdout)
|
|
|
|
|
head = self.todo._qemu_mobile_build_steps("/tmp/el")
|
|
|
|
|
head = head[: head.index("check_bundle_transfer.py")]
|
|
|
|
|
self.assertNotIn("mstep", head[-160:])
|
|
|
|
|
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
def test_a_missing_apk_fails_even_when_gradle_returns_zero(self):
|
|
|
|
|
"""L'APK est la preuve, pas le code de sortie de Gradle : une tâche peut
|
|
|
|
|
rendre 0 sans rien produire."""
|
|
|
|
|
res = self._run_steps(apk=False)
|
|
|
|
|
self.assertNotEqual(0, res.returncode)
|
|
|
|
|
self.assertIn("APK", res.stdout)
|
|
|
|
|
|
|
|
|
|
def test_a_complete_build_succeeds(self):
|
|
|
|
|
"""L'autre sens du contrat : sans lui, un test qui échoue toujours
|
|
|
|
|
passerait pour un test qui vérifie quelque chose."""
|
|
|
|
|
res = self._run_steps(apk=True)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stdout[-400:])
|
|
|
|
|
|
|
|
|
|
def test_the_swap_step_alone_never_fails_the_chain(self):
|
|
|
|
|
"""Une image btrfs refuse un fichier d'échange ordinaire, et une
|
|
|
|
|
compilation qui tient en mémoire n'en a pas besoin."""
|
|
|
|
|
res = self._run_steps(apk=True)
|
|
|
|
|
self.assertEqual(0, res.returncode)
|
|
|
|
|
self.assertNotIn("|| true", self.todo._qemu_mobile_build_steps("/x"))
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
|
|
|
|
|
def test_the_build_covers_apk_and_tests(self):
|
|
|
|
|
cmd = self.todo._qemu_mobile_remote_cmd()
|
|
|
|
|
for step in (
|
|
|
|
|
"update_manifest_local_mobile.sh",
|
|
|
|
|
"./install-android.sh",
|
|
|
|
|
"npm ci",
|
|
|
|
|
"npm run build",
|
|
|
|
|
"npx cap sync android",
|
|
|
|
|
"./gradlew --no-daemon assembleDebug",
|
|
|
|
|
"npm test",
|
|
|
|
|
):
|
|
|
|
|
self.assertIn(step, cmd, step)
|
|
|
|
|
|
|
|
|
|
def test_the_platform_comes_from_the_project(self):
|
|
|
|
|
"""L'installateur amont pose android-34, variables.gradle demande
|
|
|
|
|
compileSdk 36 : on lit le chiffre plutôt que de le figer."""
|
|
|
|
|
cmd = self.todo._qemu_mobile_remote_cmd()
|
|
|
|
|
self.assertIn("android/variables.gradle", cmd)
|
|
|
|
|
self.assertIn("platforms;android-$v", cmd)
|
|
|
|
|
self.assertNotIn("platforms;android-36", cmd)
|
|
|
|
|
|
|
|
|
|
def test_the_apk_is_the_proof(self):
|
|
|
|
|
"""Une tâche Gradle peut rendre 0 sans rien produire."""
|
|
|
|
|
cmd = self.todo._qemu_mobile_remote_cmd()
|
|
|
|
|
self.assertIn("outputs/apk/debug/*.apk", cmd)
|
|
|
|
|
|
[FIX] todo qemu: find the APK where AGP writes it, and install it as it is
The app runs in the emulator now, and two of my own checks were wrong. With an
injected ABI, AGP writes to intermediates/apk/debug, not outputs/apk/debug: a
SUCCESSFUL build was reported "no APK produced" because only the second path
was searched. That same mode marks the APK testOnly, so adb refuses it without
-t — INSTALL_FAILED_TEST_ONLY. Both measured on the emulator, where
ca.erplibre.home then installed, launched, and ran its SQLite migrations.
The manifest also stops tracking sentencepiece at master, which made the build
non-reproducible and, since August, unbuildable: master fetches protobuf and
compiles protoc for the Android target before running it on the host. v0.2.1
ships its .pb.cc pre-generated. Its own absl subset still lacks
flags/flag.cc, so that library remains to be sorted out upstream.
--- FR ---
L'application tourne dans l'émulateur, et deux de mes propres contrôles étaient
faux. Avec une ABI injectée, AGP écrit dans intermediates/apk/debug et non dans
outputs/apk/debug : une compilation RÉUSSIE était rapportée « aucun APK
produit » parce que je ne regardais que le second chemin. Ce même mode marque
l'APK testOnly, et adb le refuse sans -t — INSTALL_FAILED_TEST_ONLY. Les deux
mesurés sur l'émulateur, où ca.erplibre.home s'est ensuite installé, lancé, et
a joué ses migrations SQLite.
Le manifeste cesse aussi de suivre sentencepiece à master, ce qui rendait la
compilation non reproductible et, depuis août, impossible : master récupère
protobuf et compile protoc pour la cible Android avant de l'exécuter sur
l'hôte. La v0.2.1 livre ses .pb.cc pré-générés. Son propre sous-ensemble absl
manque encore flags/flag.cc : cette bibliothèque reste à régler en amont.
Assisted-by: Claude Opus 5
2026-08-18 02:53:17 -04:00
|
|
|
def test_both_apk_locations_are_searched(self):
|
|
|
|
|
"""Avec une ABI injectée, AGP écrit dans intermediates et non dans
|
|
|
|
|
outputs : mesuré, une compilation RÉUSSIE était rapportée « aucun APK
|
|
|
|
|
produit » parce qu'un seul des deux chemins était regardé."""
|
|
|
|
|
cmd = self.todo._qemu_mobile_remote_cmd()
|
|
|
|
|
self.assertIn("intermediates/apk/debug/*.apk", cmd)
|
|
|
|
|
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
def test_no_apk_means_non_zero(self):
|
|
|
|
|
"""Éprouvé plutôt que relu : sans APK, le bloc DOIT rendre non nul.
|
|
|
|
|
Une ligne d'information placée après le « fi » suffisait à rendre 0 et
|
|
|
|
|
à faire repasser la VM au vert."""
|
|
|
|
|
cmd = self.todo._qemu_mobile_remote_cmd()
|
|
|
|
|
tail = cmd[cmd.index("apk=$(ls") :]
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-c", "set -e\n" + tail], capture_output=True, text=True
|
|
|
|
|
)
|
|
|
|
|
self.assertNotEqual(0, res.returncode, res.stdout)
|
|
|
|
|
|
|
|
|
|
def test_every_failure_names_a_cause(self):
|
|
|
|
|
"""Un journal de dizaines de mégaoctets ne se relit pas : le diagnostic
|
|
|
|
|
doit dire pourquoi."""
|
|
|
|
|
cmd = self.todo._qemu_mobile_remote_cmd()
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
# Une entrée peut porter un 3e élément : la commande de contexte.
|
|
|
|
|
for pattern in (e[0] for e in TODO._QEMU_MOBILE_DIAG):
|
[ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.
Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.
--- FR ---
Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.
Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.
Assisted-by: Claude Opus 5
2026-08-18 00:50:08 -04:00
|
|
|
self.assertIn(pattern, cmd, pattern)
|
|
|
|
|
self.assertIn('tail -12 "$1"', cmd)
|
|
|
|
|
|
|
|
|
|
def test_heavy_output_stays_out_of_the_install_log(self):
|
|
|
|
|
"""Des centaines de lignes Gradle portant le mot « error » sans être
|
|
|
|
|
des pannes rendraient le compteur du tableau de bord inutilisable."""
|
|
|
|
|
cmd = self.todo._qemu_mobile_remote_cmd()
|
|
|
|
|
self.assertIn('>> "$M" 2>&1', cmd)
|
|
|
|
|
|
|
|
|
|
|
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.
The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.
--- FR ---
Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.
La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.
Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
|
|
|
class TestAndroidEmulator(unittest.TestCase):
|
|
|
|
|
"""Émulateur Android : visible depuis le poste par « ssh -X »."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.cmd = self.todo._qemu_avd_remote_cmd()
|
|
|
|
|
|
|
|
|
|
def test_no_desktop_needed_in_the_vm(self):
|
|
|
|
|
"""Il s'affiche sur l'écran de qui s'y connecte, pas dans la VM."""
|
|
|
|
|
self.assertFalse(TODO._QEMU_VM_TOOLS["avd"]["needs_desktop"])
|
|
|
|
|
self.assertIn(
|
|
|
|
|
"avd", self.todo._qemu_tools_for(("avd",), "amd64", "", "ubuntu")
|
|
|
|
|
)
|
|
|
|
|
|
[FIX] script todo: régler l'écran de l'émulateur au lancement, densité comprise
Écrire hw.lcd.* dans le config.ini de l'AVD ne servait à rien : l'émulateur
réécrit ce fichier depuis le profil du téléphone au premier démarrage, et l'AVD
repartait en 1080x2400 densité 420 — quatre fois les pixels voulus. Constaté sur
la VM, où le réglage était censé s'appliquer depuis des semaines.
La taille passe donc au lancement, et la DENSITÉ avec elle. C'est ce point qui
surprend, et il est mesuré sur une charge identique : 540x1140 en densité 420 est
PIRE que le plein écran — 81 ms de médiane contre 40, 57 % d'images en retard
contre 37, tout étant rendu énorme. En densité 240 : 38 ms, 32 %, et le 99e
centile tombe de 950 ms à 250.
« -no-snapshot-save » vient avec : ce menu propose de tuer l'émulateur par
pkill, et le lancement suivant mourait alors sur « A snapshot operation is
pending ». Vérifié après un pkill : plus aucun FATAL.
--- EN ---
Writing hw.lcd.* into the AVD's config.ini did nothing: the emulator rewrites
that file from the phone profile on first boot, and the AVD came back at
1080x2400 density 420 — four times the intended pixels. Found on the VM, where
the setting was supposed to have applied for weeks.
The size therefore moves to launch time, and the DENSITY with it. That is the
surprising part, measured on an identical workload: 540x1140 at density 420 is
WORSE than the full screen — 81 ms median against 40, 57 % janky frames against
37, everything rendered huge. At density 240: 38 ms, 32 %, and the 99th
percentile drops from 950 ms to 250.
"-no-snapshot-save" comes along: this menu offers to kill the emulator with
pkill, and the next start then died on "A snapshot operation is pending".
Checked after a pkill: no FATAL left.
Assisted-by: Claude Opus 5
2026-08-21 21:49:44 -04:00
|
|
|
def test_the_screen_is_set_at_launch_not_in_the_config(self):
|
|
|
|
|
"""Écrire hw.lcd.* dans config.ini ne SERT À RIEN : l'émulateur réécrit
|
|
|
|
|
ce fichier depuis le profil du téléphone au premier démarrage, et l'AVD
|
|
|
|
|
repartait en 1080x2400 densité 420 — constaté sur la VM. La taille se
|
|
|
|
|
règle donc au lancement, et la commande affichée la porte."""
|
|
|
|
|
self.assertNotIn("hw.lcd.width", self.cmd)
|
|
|
|
|
self.assertIn("-skin 540x1140", self.cmd)
|
|
|
|
|
# Ces deux clés-là survivent : elles ne viennent pas du profil.
|
|
|
|
|
self.assertIn("hw.gpu.mode=swangle", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_the_density_travels_with_the_resolution(self):
|
|
|
|
|
"""Contre-intuitif, et mesuré : 540x1140 en densité 420 est PIRE que le
|
|
|
|
|
plein écran — 81 ms de médiane contre 40, et 57 % d'images en retard
|
|
|
|
|
contre 37, tout étant rendu énorme. Avec la densité 240 : 38 ms, 32 %,
|
|
|
|
|
et le 99e centile tombe de 950 ms à 250."""
|
|
|
|
|
self.assertIn("qemu.sf.lcd_density=240", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_a_killed_emulator_does_not_block_the_next_start(self):
|
|
|
|
|
"""Ce menu propose lui-même de tuer l'émulateur par pkill. Sans
|
|
|
|
|
« -no-snapshot-save », le lancement suivant meurt sur « A snapshot
|
|
|
|
|
operation is pending and timeout has expired » — vécu, et le message ne
|
|
|
|
|
dit pas quoi faire."""
|
|
|
|
|
self.assertIn("-no-snapshot-save", self.cmd)
|
[IMP] todo qemu: shrink the emulator screen so a remote display keeps up
"It launches but it is too slow." The Pixel profile gives 1080x2400 — 2.6
megapixels pushed frame by frame through SSH, in software rendering. The AVD
now declares 540x1140 at 240 dpi: 0.62 megapixel, 4.2 times less. Measured on
the VM after the change: wm size reports 540x1140, and a full-screen capture
drops from 220 KB to 49 KB. Android handles the density and the app does not
notice; whoever wants the real size overrides it with -skin 1080x2400.
The printed command also gained -XC over -X, X11 compression being free on a
remote display, and -no-boot-anim. Checked: ca.erplibre.home relaunched at the
new size, MainActivity resumed.
--- FR ---
« Ça se lance mais c'est trop lent. » Le profil Pixel donne 1080x2400, soit
2,6 mégapixels poussés image par image dans SSH, en rendu logiciel. L'AVD
déclare maintenant 540x1140 à 240 ppp : 0,62 mégapixel, 4,2 fois moins. Mesuré
sur la VM après le changement : « wm size » rend 540x1140, et une capture plein
écran tombe de 220 Ko à 49 Ko. Android gère la densité et l'application ne s'en
aperçoit pas ; qui veut la taille réelle l'écrase par « -skin 1080x2400 ».
La commande affichée gagne aussi « -XC » au lieu de « -X », la compression X11
étant gratuite sur un écran distant, et « -no-boot-anim ». Vérifié :
ca.erplibre.home relancé à la nouvelle taille, MainActivity au premier plan.
Assisted-by: Claude Opus 5
2026-08-18 03:31:53 -04:00
|
|
|
|
|
|
|
|
def test_the_printed_command_compresses_the_display(self):
|
|
|
|
|
"""« -XC » plutôt que « -X » sur un écran distant."""
|
|
|
|
|
self.assertIn("ssh -XC erplibre@$ip", self.cmd)
|
|
|
|
|
self.assertNotIn("ssh -X erplibre@$ip", self.cmd.replace("-XC", ""))
|
|
|
|
|
|
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.
The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.
--- FR ---
Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.
La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.
Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
|
|
|
def test_software_rendering_is_written_into_the_avd(self):
|
|
|
|
|
"""Par « ssh -X » il n'y a pas de GLX direct : en « auto »,
|
|
|
|
|
l'émulateur s'ouvre sur un écran noir. Le réglage va dans config.ini
|
|
|
|
|
pour qu'« emulator -avd erplibre » suffise."""
|
[FIX] todo qemu: name a GPU mode the emulator still accepts
Reported from a workstation: "Selected GPU option 'swiftshader_indirect' is
not valid, switching to auto", then "Your GPU drivers may have a bug". That
mode no longer exists in emulator 37.1; it fell back to swangle by itself, so
it worked while printing two errors that read like a fault. emulator -help-gpu
lists exactly four modes: auto, host, swiftshader, swangle. The AVD now asks
for swangle. Checked on the VM: zero GPU errors where there were two,
vulkan:swiftshader gles:swangle selected outright, boot in 55 s.
--- FR ---
Remonté depuis un poste : « Selected GPU option 'swiftshader_indirect' is not
valid, switching to auto », puis « Your GPU drivers may have a bug ». Ce mode
n'existe plus dans l'émulateur 37.1 ; il retombait de lui-même sur swangle,
donc cela fonctionnait en affichant deux erreurs qui se lisent comme une panne.
« emulator -help-gpu » énumère exactement quatre modes : auto, host,
swiftshader, swangle. L'AVD demande désormais swangle. Vérifié sur la VM : zéro
erreur GPU là où il y en avait deux, vulkan:swiftshader gles:swangle choisis
d'emblée, boot en 55 s.
Assisted-by: Claude Opus 5
2026-08-19 02:03:42 -04:00
|
|
|
self.assertIn("hw.gpu.mode=swangle", self.cmd)
|
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.
The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.
--- FR ---
Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.
La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.
Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
|
|
|
self.assertIn("config.ini", self.cmd)
|
[FIX] todo qemu: name a GPU mode the emulator still accepts
Reported from a workstation: "Selected GPU option 'swiftshader_indirect' is
not valid, switching to auto", then "Your GPU drivers may have a bug". That
mode no longer exists in emulator 37.1; it fell back to swangle by itself, so
it worked while printing two errors that read like a fault. emulator -help-gpu
lists exactly four modes: auto, host, swiftshader, swangle. The AVD now asks
for swangle. Checked on the VM: zero GPU errors where there were two,
vulkan:swiftshader gles:swangle selected outright, boot in 55 s.
--- FR ---
Remonté depuis un poste : « Selected GPU option 'swiftshader_indirect' is not
valid, switching to auto », puis « Your GPU drivers may have a bug ». Ce mode
n'existe plus dans l'émulateur 37.1 ; il retombait de lui-même sur swangle,
donc cela fonctionnait en affichant deux erreurs qui se lisent comme une panne.
« emulator -help-gpu » énumère exactement quatre modes : auto, host,
swiftshader, swangle. L'AVD demande désormais swangle. Vérifié sur la VM : zéro
erreur GPU là où il y en avait deux, vulkan:swiftshader gles:swangle choisis
d'emblée, boot en 55 s.
Assisted-by: Claude Opus 5
2026-08-19 02:03:42 -04:00
|
|
|
# « swiftshader_indirect » n'existe plus : l'émulateur 37.1 le refuse,
|
|
|
|
|
# affiche deux erreurs et retombe sur swangle de lui-même. Mesuré.
|
|
|
|
|
self.assertNotIn("swiftshader_indirect", self.cmd)
|
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.
The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.
--- FR ---
Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.
La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.
Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
|
|
|
|
|
|
|
|
def test_xauth_is_installed(self):
|
|
|
|
|
"""Sans xauth dans la VM, « ssh -X » n'ouvre aucun affichage — et le
|
|
|
|
|
paquet manque des images cloud."""
|
|
|
|
|
self.assertIn("xauth", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_it_says_when_kvm_is_missing(self):
|
|
|
|
|
"""Un émulateur x86 sans KVM refuse de démarrer : le dire là où c'est
|
|
|
|
|
réparable, sur l'hôte, plutôt qu'au premier lancement."""
|
|
|
|
|
self.assertIn("/dev/kvm", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_the_pixel_is_chosen_at_runtime(self):
|
|
|
|
|
"""« le plus récent, le plus petit écran » se demande au SDK : figer un
|
|
|
|
|
modèle le rendrait faux à la prochaine génération."""
|
|
|
|
|
self.assertIn("avdmanager list device", self.cmd)
|
|
|
|
|
self.assertIn("pixel_", self.cmd)
|
|
|
|
|
self.assertIn("pro|xl|fold|tablet", self.cmd)
|
|
|
|
|
self.assertIn("sort -t_ -k2 -n", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_the_system_image_falls_back(self):
|
|
|
|
|
"""Google ne publie pas d'image pour toutes les API : on descend."""
|
|
|
|
|
self.assertIn("for a in $v 36 35 34", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_it_prints_the_command_to_open_it(self):
|
|
|
|
|
"""Un émulateur dont on ignore comment l'ouvrir ne sert à personne."""
|
[IMP] todo qemu: shrink the emulator screen so a remote display keeps up
"It launches but it is too slow." The Pixel profile gives 1080x2400 — 2.6
megapixels pushed frame by frame through SSH, in software rendering. The AVD
now declares 540x1140 at 240 dpi: 0.62 megapixel, 4.2 times less. Measured on
the VM after the change: wm size reports 540x1140, and a full-screen capture
drops from 220 KB to 49 KB. Android handles the density and the app does not
notice; whoever wants the real size overrides it with -skin 1080x2400.
The printed command also gained -XC over -X, X11 compression being free on a
remote display, and -no-boot-anim. Checked: ca.erplibre.home relaunched at the
new size, MainActivity resumed.
--- FR ---
« Ça se lance mais c'est trop lent. » Le profil Pixel donne 1080x2400, soit
2,6 mégapixels poussés image par image dans SSH, en rendu logiciel. L'AVD
déclare maintenant 540x1140 à 240 ppp : 0,62 mégapixel, 4,2 fois moins. Mesuré
sur la VM après le changement : « wm size » rend 540x1140, et une capture plein
écran tombe de 220 Ko à 49 Ko. Android gère la densité et l'application ne s'en
aperçoit pas ; qui veut la taille réelle l'écrase par « -skin 1080x2400 ».
La commande affichée gagne aussi « -XC » au lieu de « -X », la compression X11
étant gratuite sur un écran distant, et « -no-boot-anim ». Vérifié :
ca.erplibre.home relancé à la nouvelle taille, MainActivity au premier plan.
Assisted-by: Claude Opus 5
2026-08-18 03:31:53 -04:00
|
|
|
self.assertIn("ssh -XC erplibre@$ip", self.cmd)
|
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.
The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.
--- FR ---
Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.
La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.
Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
|
|
|
self.assertIn("adb install -r", self.cmd)
|
|
|
|
|
|
[FIX] todo qemu: the printed emulator command could not run as printed
Reported from a workstation: "emulator: command not found". The line the
emulator step printed relied on PATH, and "ssh host 'command'" reads neither
~/.profile nor ~/.bashrc — Ubuntu even opens the latter with a return for
non-interactive shells. So what install-android.sh writes there never applies
to those commands. Both printed lines now carry absolute paths.
Behind it, a second one: the emulator ships TWO qemu binaries and only the
headless one does without PulseAudio. The windowed one — the ssh -X case —
links libpulse.so.0, absent from cloud images, and stops on "cannot open
shared object file" even with -no-audio. Measured: it is the only missing
library, the Qt dependencies travel in the bundle. Checked after the fix: the
windowed binary starts and reports its version.
--- FR ---
Remonté depuis un poste : « emulator: command not found ». La ligne affichée
par l'étape émulateur comptait sur le PATH, or « ssh hôte 'commande' » ne lit
ni ~/.profile ni ~/.bashrc — Ubuntu ouvre même le second par un return pour
les shells non interactifs. Ce que install-android.sh y écrit ne s'applique
donc jamais à ces commandes. Les deux lignes affichées portent maintenant des
chemins absolus.
Derrière, une seconde : l'émulateur livre DEUX binaires qemu et seul le
headless se passe de PulseAudio. Celui qui ouvre une fenêtre — le cas du
ssh -X — lie libpulse.so.0, absente des images cloud, et s'arrête sur
« cannot open shared object file » même avec -no-audio. Mesuré : c'est la
seule bibliothèque qui manque, les dépendances Qt voyagent dans le bundle.
Vérifié après correction : le binaire fenêtré démarre et donne sa version.
Assisted-by: Claude Opus 5
2026-08-18 02:30:02 -04:00
|
|
|
def test_the_printed_commands_use_absolute_paths(self):
|
|
|
|
|
"""« ssh hôte 'commande' » ne lit ni ~/.profile ni ~/.bashrc — Ubuntu y
|
|
|
|
|
met même un « return » pour les shells non interactifs. Une commande
|
|
|
|
|
affichée qui compte sur le PATH répond « command not found ». Vécu."""
|
|
|
|
|
self.assertIn("$HOME/android/emulator/emulator", self.cmd)
|
|
|
|
|
self.assertIn("$HOME/android/platform-tools/adb", self.cmd)
|
|
|
|
|
self.assertNotIn('"emulator -avd', self.cmd)
|
|
|
|
|
self.assertNotIn('"adb install', self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_the_windowed_emulator_gets_its_audio_library(self):
|
|
|
|
|
"""Deux binaires qemu : seul le « headless » se passe de PulseAudio.
|
|
|
|
|
Celui qui ouvre une fenêtre lie libpulse.so.0, absente des images
|
|
|
|
|
cloud, et échoue même avec « -no-audio »."""
|
|
|
|
|
self.assertIn("libpulse0", self.cmd)
|
|
|
|
|
|
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.
The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.
--- FR ---
Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.
La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.
Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
|
|
|
def test_one_prologue_and_one_sdk_for_both_options(self):
|
|
|
|
|
"""Deux prologues, et le second tronquerait le journal du premier."""
|
|
|
|
|
both = self.todo._qemu_after_remote_cmd(("mobile", "avd"))
|
|
|
|
|
self.assertEqual(1, both.count("mstep() {"))
|
|
|
|
|
self.assertEqual(1, both.count('M="$HOME/erplibre-mobile-build.log"'))
|
|
|
|
|
|
|
|
|
|
def test_the_emulator_cannot_mask_a_build_failure(self):
|
|
|
|
|
"""ÉPROUVÉ, pas relu. Sans accolades autour de chaque groupe, « && » ne
|
|
|
|
|
lie que la première commande du suivant : mesuré sur une VM, un APK
|
|
|
|
|
manquant laissait tourner l'émulateur puis rendait 0 — la VM repassait
|
|
|
|
|
au vert alors que rien n'avait compilé."""
|
|
|
|
|
both = self.todo._qemu_after_remote_cmd(("mobile", "avd"))
|
|
|
|
|
# On neutralise les étapes : seul le CHAÎNAGE est en cause ici.
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
#
|
|
|
|
|
# « sudo » est neutralisé AUSSI, et ce n'est pas décoratif : le bloc
|
|
|
|
|
# ajoute un fichier d'échange de 4 Go et une ligne à /etc/fstab. Sans
|
|
|
|
|
# ce bouchon, un test le ferait sur la machine qui l'exécute.
|
|
|
|
|
stub = (
|
|
|
|
|
'mstep() { echo " -> $1"; return 0; }; mdiag() { :; }; '
|
|
|
|
|
"sudo() { return 0; }; "
|
|
|
|
|
)
|
|
|
|
|
# Ancre robuste : on part du journal mobile et on remonte à l'accolade
|
|
|
|
|
# qui ouvre son groupe. Chercher « { mstep » liait ce test à la forme
|
|
|
|
|
# de la PREMIÈRE étape, et l'ajout du swap devant l'a cassé.
|
|
|
|
|
marker = both.index("erplibre-mobile-build.log")
|
|
|
|
|
tail = both[both.rindex("{ ", 0, marker) :]
|
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.
The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.
--- FR ---
Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.
La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.
Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-c", "set -e; " + stub + tail],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
self.assertNotEqual(0, res.returncode, res.stdout)
|
|
|
|
|
# Et l'émulateur ne doit PAS avoir été touché.
|
|
|
|
|
self.assertNotIn("Pixel", res.stdout)
|
|
|
|
|
|
|
|
|
|
def test_valid_shell_in_every_combination(self):
|
|
|
|
|
for tools in (("mobile",), ("avd",), ("mobile", "avd")):
|
|
|
|
|
cmd = self.todo._qemu_after_remote_cmd(tools)
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-n"], input=cmd, text=True, capture_output=True
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, res.returncode, f"{tools}: {res.stderr}")
|
|
|
|
|
|
|
|
|
|
def test_no_diagnostic_pattern_carries_an_apostrophe(self):
|
|
|
|
|
"""Ces motifs partent dans un « grep -q '<motif>' » : une apostrophe
|
|
|
|
|
fermait la chaîne et rendait tout le bloc invalide. Vécu."""
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
# Une entrée peut porter un 3e élément : la commande de contexte.
|
|
|
|
|
for pattern in (e[0] for e in TODO._QEMU_MOBILE_DIAG):
|
[ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.
The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.
--- FR ---
Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.
La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.
Assisted-by: Claude Opus 5
2026-08-18 02:17:08 -04:00
|
|
|
self.assertNotIn("'", pattern, pattern)
|
|
|
|
|
|
|
|
|
|
|
[FIX] todo qemu: open the PyCharm project without a screen, and unlicensed
Two measurements, one after the other. The unified PyCharm that
code=PCC&latest now serves stops on its licence: the log says
NoValidIdeLicense then "Get licenses: request requires authentication", and
no project is ever opened — so no .idea, so nothing for the install to
configure. The Community line asks for no account and is still patched
(2025.2.6.2 on 2026-07-29); it is resolved from the release feed, so no
version is frozen here.
That build then froze 1.3 s after startup: the trust dialog, invisible under
Xvfb and waiting for a click. idea.trust.all.projects unblocks it. Checked
on an Ubuntu 26.04 VM: .idea complete in 195 s, and pycharm_configuration.py
writes its exclusions into erplibre.iml.
--- FR ---
Deux mesures, l'une après l'autre. Le PyCharm unifié que sert désormais
code=PCC&latest s'arrête sur sa licence : le journal dit NoValidIdeLicense
puis « Get licenses: request requires authentication », et aucun projet ne
s'ouvre — donc pas de .idea, donc rien à configurer pour l'installation. La
ligne Community ne demande aucun compte et reste corrigée (2025.2.6.2 le
2026-07-29) ; elle est résolue depuis le flux des versions, sans qu'aucun
numéro ne soit figé ici.
Ce build se figeait ensuite 1,3 s après le démarrage : la fenêtre de
confiance, invisible sous Xvfb et attendant un clic.
idea.trust.all.projects la lève. Vérifié sur une VM Ubuntu 26.04 : .idea
complet en 195 s, et pycharm_configuration.py écrit ses exclusions dans
erplibre.iml.
Assisted-by: Claude Opus 5
2026-08-17 23:22:38 -04:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
[FIX] script todo: démarrer le bureau, pas seulement l'activer
Une VM graphique restait sur une console texte jusqu'au premier redémarrage.
GNOME installé, gdm3 installé, graphical.target par défaut, lien
display-manager.service posé par le paquet — et rien à l'écran. Deux causes
superposées, mesurées sur erplibre-ubuntu-2604-gnome :
graphical.target était DÉJÀ atteinte quand le paquet est arrivé, et une cible
active ne rattrape pas un service ajouté après coup. Et « systemctl enable gdm »
rend 0 sans rien faire sur Debian et Ubuntu : l'unité n'a pas de « WantedBy »,
seulement l'alias que le paquet pose lui-même.
Le bureau est donc démarré, avec repli sur le service de la saveur, et l'échec
se dit au lieu de se taire. Vérifié : bureau arrêté puis fragment rejoué ->
gnome-shell revient, écran de connexion GDM à l'image.
--- EN ---
A graphical VM stayed on a text console until its first reboot. GNOME
installed, gdm3 installed, graphical.target the default, the
display-manager.service alias in place by the package — and nothing on screen.
Two causes stacked, measured on erplibre-ubuntu-2604-gnome:
graphical.target had ALREADY been reached when the package arrived, and an
active target does not pick up a service added afterwards. And "systemctl enable
gdm" returns 0 doing nothing on Debian and Ubuntu: the unit has no "WantedBy",
only the alias the package installs itself.
The desktop is therefore started, with a fallback to the flavour's service, and
a failure says so instead of staying quiet. Verified: desktop stopped then the
fragment replayed -> gnome-shell back, GDM greeter on screen.
Assisted-by: Claude Opus 5
2026-08-20 02:56:21 -04:00
|
|
|
class TestTheDesktopActuallyStarts(unittest.TestCase):
|
|
|
|
|
"""Installer un bureau ne suffit pas : il faut le DÉMARRER.
|
|
|
|
|
|
|
|
|
|
Vécu sur erplibre-ubuntu-2604-gnome, et le diagnostic ne sautait pas aux
|
|
|
|
|
yeux : GNOME installé, gdm3 installé, graphical.target par défaut, lien
|
|
|
|
|
display-manager.service en place — et la console de la VM restait en mode
|
|
|
|
|
texte. Deux causes superposées :
|
|
|
|
|
|
|
|
|
|
- graphical.target était DÉJÀ atteinte quand le paquet est arrivé, et une
|
|
|
|
|
cible active ne rattrape pas un service ajouté après coup ;
|
|
|
|
|
- « systemctl enable gdm » rend 0 sans rien faire sur Debian et Ubuntu :
|
|
|
|
|
l'unité n'a pas de « WantedBy », seulement un alias que le paquet pose.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.cmd = self.todo._qemu_desktop_remote_cmd("gnome", "deb")
|
|
|
|
|
|
|
|
|
|
def _start_block(self, cmd=None):
|
|
|
|
|
"""Le seul « if » qui démarre le bureau, extrait tel quel."""
|
|
|
|
|
cmd = cmd or self.cmd
|
|
|
|
|
start = cmd.index("if sudo systemctl start display-manager")
|
|
|
|
|
return cmd[start : cmd.index("fi; ", start) + 4]
|
|
|
|
|
|
|
|
|
|
def test_it_starts_and_does_not_only_enable(self):
|
|
|
|
|
self.assertIn("systemctl start display-manager.service", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_it_falls_back_to_the_desktop_service(self):
|
|
|
|
|
"""« display-manager.service » est un alias que les paquets Debian
|
|
|
|
|
posent ; ailleurs c'est « gdm » qui porte le WantedBy."""
|
|
|
|
|
block = self._start_block()
|
|
|
|
|
self.assertIn("systemctl start gdm", block)
|
|
|
|
|
|
|
|
|
|
def test_it_comes_after_the_default_target_and_before_xrdp(self):
|
|
|
|
|
self.assertLess(
|
|
|
|
|
self.cmd.index("set-default graphical.target"),
|
|
|
|
|
self.cmd.index("start display-manager.service"),
|
|
|
|
|
)
|
|
|
|
|
self.assertLess(
|
|
|
|
|
self.cmd.index("start display-manager.service"),
|
|
|
|
|
self.cmd.index("command -v xrdp"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _run(self, systemctl_body):
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
bin_dir = pathlib.Path(tmp) / "bin"
|
|
|
|
|
bin_dir.mkdir()
|
|
|
|
|
(bin_dir / "sudo").write_text('#!/bin/bash\nexec "$@"\n')
|
|
|
|
|
(bin_dir / "systemctl").write_text(
|
|
|
|
|
f"#!/bin/bash\n{systemctl_body}\n"
|
|
|
|
|
)
|
|
|
|
|
for n in ("sudo", "systemctl"):
|
|
|
|
|
(bin_dir / n).chmod(0o755)
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-c", self._start_block()],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
env=dict(os.environ, PATH=f"{bin_dir}:/usr/bin:/bin"),
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
return res.stdout
|
|
|
|
|
|
|
|
|
|
def test_the_alias_path_reports_a_started_session(self):
|
|
|
|
|
out = self._run("exit 0")
|
|
|
|
|
self.assertIn("session", out.lower())
|
|
|
|
|
self.assertNotIn("⚠", out)
|
|
|
|
|
|
|
|
|
|
def test_the_fallback_path_also_reports_started(self):
|
|
|
|
|
"""display-manager absent, gdm présent : c'est le cas d'Arch."""
|
|
|
|
|
out = self._run(
|
|
|
|
|
'case "$*" in *display-manager*) exit 1;; *) exit 0;; esac'
|
|
|
|
|
)
|
|
|
|
|
self.assertNotIn("⚠", out)
|
|
|
|
|
|
|
|
|
|
def test_when_nothing_starts_it_says_to_reboot(self):
|
|
|
|
|
"""Le pire serait de se taire : l'utilisateur cherche un écran."""
|
|
|
|
|
out = self._run("exit 1")
|
|
|
|
|
self.assertIn("⚠", out)
|
|
|
|
|
self.assertIn("boot", out.lower() + "reboot")
|
|
|
|
|
|
|
|
|
|
def test_the_block_is_valid_shell(self):
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-n"],
|
|
|
|
|
input=self._start_block(),
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stderr)
|
|
|
|
|
|
|
|
|
|
|
[FIX] todo qemu: open the PyCharm project without a screen, and unlicensed
Two measurements, one after the other. The unified PyCharm that
code=PCC&latest now serves stops on its licence: the log says
NoValidIdeLicense then "Get licenses: request requires authentication", and
no project is ever opened — so no .idea, so nothing for the install to
configure. The Community line asks for no account and is still patched
(2025.2.6.2 on 2026-07-29); it is resolved from the release feed, so no
version is frozen here.
That build then froze 1.3 s after startup: the trust dialog, invisible under
Xvfb and waiting for a click. idea.trust.all.projects unblocks it. Checked
on an Ubuntu 26.04 VM: .idea complete in 195 s, and pycharm_configuration.py
writes its exclusions into erplibre.iml.
--- FR ---
Deux mesures, l'une après l'autre. Le PyCharm unifié que sert désormais
code=PCC&latest s'arrête sur sa licence : le journal dit NoValidIdeLicense
puis « Get licenses: request requires authentication », et aucun projet ne
s'ouvre — donc pas de .idea, donc rien à configurer pour l'installation. La
ligne Community ne demande aucun compte et reste corrigée (2025.2.6.2 le
2026-07-29) ; elle est résolue depuis le flux des versions, sans qu'aucun
numéro ne soit figé ici.
Ce build se figeait ensuite 1,3 s après le démarrage : la fenêtre de
confiance, invisible sous Xvfb et attendant un clic.
idea.trust.all.projects la lève. Vérifié sur une VM Ubuntu 26.04 : .idea
complet en 195 s, et pycharm_configuration.py écrit ses exclusions dans
erplibre.iml.
Assisted-by: Claude Opus 5
2026-08-17 23:22:38 -04:00
|
|
|
class TestPycharmFirstOpen(unittest.TestCase):
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
"""Ouverture sans écran, pour que le .idea existe avant l'installation.
|
|
|
|
|
|
|
|
|
|
Deux défauts vécus sur erplibre-ubuntu-2604-gnome, tous deux silencieux :
|
|
|
|
|
l'IDE restait vivant 45 minutes après l'étape avec 1,9 Go — « $! » est le
|
|
|
|
|
PID de xvfb-run, un script, et le tuer n'atteint ni PyCharm ni Xvfb — puis
|
|
|
|
|
la compilation de l'APK qui suivait s'est fait tuer par le noyau. Et le
|
|
|
|
|
.idea n'était jamais écrit : 123 000 fichiers d'assets épuisent les watches
|
|
|
|
|
inotify, dont la limite valait 65 536.
|
|
|
|
|
"""
|
[FIX] todo qemu: open the PyCharm project without a screen, and unlicensed
Two measurements, one after the other. The unified PyCharm that
code=PCC&latest now serves stops on its licence: the log says
NoValidIdeLicense then "Get licenses: request requires authentication", and
no project is ever opened — so no .idea, so nothing for the install to
configure. The Community line asks for no account and is still patched
(2025.2.6.2 on 2026-07-29); it is resolved from the release feed, so no
version is frozen here.
That build then froze 1.3 s after startup: the trust dialog, invisible under
Xvfb and waiting for a click. idea.trust.all.projects unblocks it. Checked
on an Ubuntu 26.04 VM: .idea complete in 195 s, and pycharm_configuration.py
writes its exclusions into erplibre.iml.
--- FR ---
Deux mesures, l'une après l'autre. Le PyCharm unifié que sert désormais
code=PCC&latest s'arrête sur sa licence : le journal dit NoValidIdeLicense
puis « Get licenses: request requires authentication », et aucun projet ne
s'ouvre — donc pas de .idea, donc rien à configurer pour l'installation. La
ligne Community ne demande aucun compte et reste corrigée (2025.2.6.2 le
2026-07-29) ; elle est résolue depuis le flux des versions, sans qu'aucun
numéro ne soit figé ici.
Ce build se figeait ensuite 1,3 s après le démarrage : la fenêtre de
confiance, invisible sous Xvfb et attendant un clic.
idea.trust.all.projects la lève. Vérifié sur une VM Ubuntu 26.04 : .idea
complet en 195 s, et pycharm_configuration.py écrit ses exclusions dans
erplibre.iml.
Assisted-by: Claude Opus 5
2026-08-17 23:22:38 -04:00
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.cmd = self.todo._qemu_pycharm_project_cmd()
|
|
|
|
|
|
[FIX] script todo: ouvrir PyCharm après l'installation, pas avant
« ⚠ pas de .idea » à chaque installation : PyCharm ouvrait un dépôt cloné mais
pas installé, son configurateur d'interpréteur Python échouait faute de venv, et
il renonçait avant d'écrire quoi que ce soit. Le même appel sur un dépôt
installé écrit erplibre.iml, misc.xml, modules.xml et vcs.xml en cinq minutes —
mesuré sur la VM.
L'ouverture passe donc après le make, et « make pycharm_configure » la suit :
pycharm_update() s'était déjà exécuté pendant l'installation, quand il n'y avait
rien à configurer. Le groupe rend toujours 0 — un bonus ne rougit pas une VM —
et la phase mobile, qui porte le verdict, reste après lui.
--- EN ---
"⚠ no .idea" on every install: PyCharm was opening a repo that was cloned but
not installed, its Python interpreter configurator failed for lack of a venv,
and it gave up before writing anything. The same call on an installed repo
writes erplibre.iml, misc.xml, modules.xml and vcs.xml in five minutes —
measured on the VM.
The open therefore moves after the make, with "make pycharm_configure" behind
it: pycharm_update() had already run during the install, when there was nothing
to configure. The group always returns 0 — a bonus does not redden a VM — and
the mobile phase, which carries the verdict, still comes after it.
Assisted-by: Claude Opus 5
2026-08-19 07:50:19 -04:00
|
|
|
def test_it_runs_after_the_install_not_before(self):
|
|
|
|
|
"""Mesuré : sur un dépôt cloné mais pas installé, PyCharm n'écrit AUCUN
|
|
|
|
|
.idea — son configurateur d'interpréteur échoue faute de venv, et il
|
|
|
|
|
renonce (« ⚠ pas de .idea », deux fois sur une VM réelle). Le même appel
|
|
|
|
|
sur un dépôt installé l'écrit en cinq minutes."""
|
[FIX] todo qemu: open the PyCharm project without a screen, and unlicensed
Two measurements, one after the other. The unified PyCharm that
code=PCC&latest now serves stops on its licence: the log says
NoValidIdeLicense then "Get licenses: request requires authentication", and
no project is ever opened — so no .idea, so nothing for the install to
configure. The Community line asks for no account and is still patched
(2025.2.6.2 on 2026-07-29); it is resolved from the release feed, so no
version is frozen here.
That build then froze 1.3 s after startup: the trust dialog, invisible under
Xvfb and waiting for a click. idea.trust.all.projects unblocks it. Checked
on an Ubuntu 26.04 VM: .idea complete in 195 s, and pycharm_configuration.py
writes its exclusions into erplibre.iml.
--- FR ---
Deux mesures, l'une après l'autre. Le PyCharm unifié que sert désormais
code=PCC&latest s'arrête sur sa licence : le journal dit NoValidIdeLicense
puis « Get licenses: request requires authentication », et aucun projet ne
s'ouvre — donc pas de .idea, donc rien à configurer pour l'installation. La
ligne Community ne demande aucun compte et reste corrigée (2025.2.6.2 le
2026-07-29) ; elle est résolue depuis le flux des versions, sans qu'aucun
numéro ne soit figé ici.
Ce build se figeait ensuite 1,3 s après le démarrage : la fenêtre de
confiance, invisible sous Xvfb et attendant un clic.
idea.trust.all.projects la lève. Vérifié sur une VM Ubuntu 26.04 : .idea
complet en 195 s, et pycharm_configuration.py écrit ses exclusions dans
erplibre.iml.
Assisted-by: Claude Opus 5
2026-08-17 23:22:38 -04:00
|
|
|
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(
|
[FIX] script todo: ouvrir PyCharm après l'installation, pas avant
« ⚠ pas de .idea » à chaque installation : PyCharm ouvrait un dépôt cloné mais
pas installé, son configurateur d'interpréteur Python échouait faute de venv, et
il renonçait avant d'écrire quoi que ce soit. Le même appel sur un dépôt
installé écrit erplibre.iml, misc.xml, modules.xml et vcs.xml en cinq minutes —
mesuré sur la VM.
L'ouverture passe donc après le make, et « make pycharm_configure » la suit :
pycharm_update() s'était déjà exécuté pendant l'installation, quand il n'y avait
rien à configurer. Le groupe rend toujours 0 — un bonus ne rougit pas une VM —
et la phase mobile, qui porte le verdict, reste après lui.
--- EN ---
"⚠ no .idea" on every install: PyCharm was opening a repo that was cloned but
not installed, its Python interpreter configurator failed for lack of a venv,
and it gave up before writing anything. The same call on an installed repo
writes erplibre.iml, misc.xml, modules.xml and vcs.xml in five minutes —
measured on the VM.
The open therefore moves after the make, with "make pycharm_configure" behind
it: pycharm_update() had already run during the install, when there was nothing
to configure. The group always returns 0 — a bonus does not redden a VM — and
the mobile phase, which carries the verdict, still comes after it.
Assisted-by: Claude Opus 5
2026-08-19 07:50:19 -04:00
|
|
|
script.index("make install_os"), script.index("xvfb-run")
|
|
|
|
|
)
|
|
|
|
|
|
[FIX] script todo: configurer le projet PyCharm avec le venv, et réessayer
Deux défauts empêchaient le .idea d'exister. La configuration d'abord :
« make pycharm_configure » lance le script avec le python SYSTÈME, qui n'a pas
xmltodict — « ModuleNotFoundError », mesuré. update_env_version.pycharm_update()
l'appelle depuis .venv.erplibre ; la cible make et l'étape font désormais pareil.
L'ouverture ensuite : la première tentative sur un dépôt neuf peut n'écrire
aucun .idea, son configurateur d'interpréteur plantant sur « homeDir is null »,
là où la suivante l'écrit en 25 s — constaté sur deux VM. L'étape retente donc
une fois, en gardant les deux journaux. Vérifié sur erplibre-ubuntu-2604-gnome,
caches effacés : erplibre.iml, misc.xml, modules.xml, vcs.xml, et 0 processus
survivant.
--- EN ---
Two defects kept .idea from existing. The configuration first: "make
pycharm_configure" runs the script with the SYSTEM python, which lacks
xmltodict — "ModuleNotFoundError", measured. update_env_version.pycharm_update()
calls it from .venv.erplibre; the make target and the step now do the same.
The open next: the first attempt on a fresh repo can write no .idea at all, its
interpreter configurator dying on "homeDir is null", where the next one writes
it in 25 s — seen on two VMs. The step therefore retries once, keeping both
logs. Verified on erplibre-ubuntu-2604-gnome with caches wiped: erplibre.iml,
misc.xml, modules.xml, vcs.xml, and 0 surviving processes.
Assisted-by: Claude Opus 5
2026-08-19 08:11:12 -04:00
|
|
|
def test_the_configuration_uses_the_repo_venv(self):
|
|
|
|
|
"""Le script importe xmltodict, qui vit dans .venv.erplibre. Appelé par
|
|
|
|
|
le python système — ce que faisait « make pycharm_configure » — il
|
|
|
|
|
s'arrête sur « No module named 'xmltodict' », mesuré sur la VM.
|
|
|
|
|
update_env_version.pycharm_update() l'appelle déjà avec le venv."""
|
|
|
|
|
script = self.todo._qemu_erplibre_remote_cmd(
|
|
|
|
|
"develop", None, False, "gnome", "", "deb", ("pycharm",)
|
|
|
|
|
)
|
|
|
|
|
self.assertIn(
|
|
|
|
|
"./.venv.erplibre/bin/python"
|
|
|
|
|
" ./script/ide/pycharm_configuration.py --init",
|
|
|
|
|
script,
|
|
|
|
|
)
|
|
|
|
|
# « make pycharm_configure » reste cité dans le message d'aide — la
|
|
|
|
|
# cible est réparée, elle aussi — mais n'est plus ce qu'on EXÉCUTE.
|
|
|
|
|
self.assertNotIn("&& make pycharm_configure", script)
|
|
|
|
|
self.assertNotIn("; make pycharm_configure", script)
|
|
|
|
|
|
|
|
|
|
def test_the_open_gets_a_second_chance(self):
|
|
|
|
|
"""Mesuré sur deux VM : la première ouverture d'un dépôt neuf peut
|
|
|
|
|
n'écrire AUCUN .idea — son configurateur d'interpréteur plante
|
|
|
|
|
(« homeDir is null ») — là où la suivante l'écrit en 25 s."""
|
|
|
|
|
self.assertIn("for attempt in 1 2", self.cmd)
|
|
|
|
|
self.assertIn('[ "$ok" = 1 ] && break', self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_both_attempts_keep_their_log(self):
|
|
|
|
|
"""Tronquer à chaque tentative effacerait la trace de la première, la
|
|
|
|
|
seule qui porte la cause."""
|
|
|
|
|
self.assertIn(": > /tmp/pycharm-first-run.log", self.cmd)
|
|
|
|
|
self.assertIn(">> /tmp/pycharm-first-run.log", self.cmd)
|
|
|
|
|
|
[FIX] script todo: ouvrir PyCharm après l'installation, pas avant
« ⚠ pas de .idea » à chaque installation : PyCharm ouvrait un dépôt cloné mais
pas installé, son configurateur d'interpréteur Python échouait faute de venv, et
il renonçait avant d'écrire quoi que ce soit. Le même appel sur un dépôt
installé écrit erplibre.iml, misc.xml, modules.xml et vcs.xml en cinq minutes —
mesuré sur la VM.
L'ouverture passe donc après le make, et « make pycharm_configure » la suit :
pycharm_update() s'était déjà exécuté pendant l'installation, quand il n'y avait
rien à configurer. Le groupe rend toujours 0 — un bonus ne rougit pas une VM —
et la phase mobile, qui porte le verdict, reste après lui.
--- EN ---
"⚠ no .idea" on every install: PyCharm was opening a repo that was cloned but
not installed, its Python interpreter configurator failed for lack of a venv,
and it gave up before writing anything. The same call on an installed repo
writes erplibre.iml, misc.xml, modules.xml and vcs.xml in five minutes —
measured on the VM.
The open therefore moves after the make, with "make pycharm_configure" behind
it: pycharm_update() had already run during the install, when there was nothing
to configure. The group always returns 0 — a bonus does not redden a VM — and
the mobile phase, which carries the verdict, still comes after it.
Assisted-by: Claude Opus 5
2026-08-19 07:50:19 -04:00
|
|
|
def test_the_configuration_is_asked_for_after_the_open(self):
|
|
|
|
|
"""L'installation est déjà passée quand le .idea naît : pycharm_update()
|
|
|
|
|
n'avait rien à configurer, donc on le demande explicitement."""
|
|
|
|
|
script = self.todo._qemu_erplibre_remote_cmd(
|
|
|
|
|
"develop", None, False, "gnome", "", "deb", ("pycharm",)
|
|
|
|
|
)
|
|
|
|
|
self.assertIn("make pycharm_configure", script)
|
|
|
|
|
self.assertLess(
|
|
|
|
|
script.index("xvfb-run"), script.index("make pycharm_configure")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_it_never_decides_the_verdict_of_the_vm(self):
|
|
|
|
|
"""Un bonus : ni son échec ni celui de sa configuration ne doivent
|
|
|
|
|
rougir une VM dont tout le reste a réussi. La phase mobile, elle, porte
|
|
|
|
|
bien le verdict — et elle vient après."""
|
|
|
|
|
script = self.todo._qemu_erplibre_remote_cmd(
|
|
|
|
|
"develop", None, False, "gnome", "", "deb", ("pycharm", "mobile")
|
|
|
|
|
)
|
[FIX] script todo: configurer le projet PyCharm avec le venv, et réessayer
Deux défauts empêchaient le .idea d'exister. La configuration d'abord :
« make pycharm_configure » lance le script avec le python SYSTÈME, qui n'a pas
xmltodict — « ModuleNotFoundError », mesuré. update_env_version.pycharm_update()
l'appelle depuis .venv.erplibre ; la cible make et l'étape font désormais pareil.
L'ouverture ensuite : la première tentative sur un dépôt neuf peut n'écrire
aucun .idea, son configurateur d'interpréteur plantant sur « homeDir is null »,
là où la suivante l'écrit en 25 s — constaté sur deux VM. L'étape retente donc
une fois, en gardant les deux journaux. Vérifié sur erplibre-ubuntu-2604-gnome,
caches effacés : erplibre.iml, misc.xml, modules.xml, vcs.xml, et 0 processus
survivant.
--- EN ---
Two defects kept .idea from existing. The configuration first: "make
pycharm_configure" runs the script with the SYSTEM python, which lacks
xmltodict — "ModuleNotFoundError", measured. update_env_version.pycharm_update()
calls it from .venv.erplibre; the make target and the step now do the same.
The open next: the first attempt on a fresh repo can write no .idea at all, its
interpreter configurator dying on "homeDir is null", where the next one writes
it in 25 s — seen on two VMs. The step therefore retries once, keeping both
logs. Verified on erplibre-ubuntu-2604-gnome with caches wiped: erplibre.iml,
misc.xml, modules.xml, vcs.xml, and 0 surviving processes.
Assisted-by: Claude Opus 5
2026-08-19 08:11:12 -04:00
|
|
|
self.assertIn("pycharm_configuration.py --init || true", script)
|
[FIX] script todo: ouvrir PyCharm après l'installation, pas avant
« ⚠ pas de .idea » à chaque installation : PyCharm ouvrait un dépôt cloné mais
pas installé, son configurateur d'interpréteur Python échouait faute de venv, et
il renonçait avant d'écrire quoi que ce soit. Le même appel sur un dépôt
installé écrit erplibre.iml, misc.xml, modules.xml et vcs.xml en cinq minutes —
mesuré sur la VM.
L'ouverture passe donc après le make, et « make pycharm_configure » la suit :
pycharm_update() s'était déjà exécuté pendant l'installation, quand il n'y avait
rien à configurer. Le groupe rend toujours 0 — un bonus ne rougit pas une VM —
et la phase mobile, qui porte le verdict, reste après lui.
--- EN ---
"⚠ no .idea" on every install: PyCharm was opening a repo that was cloned but
not installed, its Python interpreter configurator failed for lack of a venv,
and it gave up before writing anything. The same call on an installed repo
writes erplibre.iml, misc.xml, modules.xml and vcs.xml in five minutes —
measured on the VM.
The open therefore moves after the make, with "make pycharm_configure" behind
it: pycharm_update() had already run during the install, when there was nothing
to configure. The group always returns 0 — a bonus does not redden a VM — and
the mobile phase, which carries the verdict, still comes after it.
Assisted-by: Claude Opus 5
2026-08-19 07:50:19 -04:00
|
|
|
self.assertLess(
|
[FIX] script todo: configurer le projet PyCharm avec le venv, et réessayer
Deux défauts empêchaient le .idea d'exister. La configuration d'abord :
« make pycharm_configure » lance le script avec le python SYSTÈME, qui n'a pas
xmltodict — « ModuleNotFoundError », mesuré. update_env_version.pycharm_update()
l'appelle depuis .venv.erplibre ; la cible make et l'étape font désormais pareil.
L'ouverture ensuite : la première tentative sur un dépôt neuf peut n'écrire
aucun .idea, son configurateur d'interpréteur plantant sur « homeDir is null »,
là où la suivante l'écrit en 25 s — constaté sur deux VM. L'étape retente donc
une fois, en gardant les deux journaux. Vérifié sur erplibre-ubuntu-2604-gnome,
caches effacés : erplibre.iml, misc.xml, modules.xml, vcs.xml, et 0 processus
survivant.
--- EN ---
Two defects kept .idea from existing. The configuration first: "make
pycharm_configure" runs the script with the SYSTEM python, which lacks
xmltodict — "ModuleNotFoundError", measured. update_env_version.pycharm_update()
calls it from .venv.erplibre; the make target and the step now do the same.
The open next: the first attempt on a fresh repo can write no .idea at all, its
interpreter configurator dying on "homeDir is null", where the next one writes
it in 25 s — seen on two VMs. The step therefore retries once, keeping both
logs. Verified on erplibre-ubuntu-2604-gnome with caches wiped: erplibre.iml,
misc.xml, modules.xml, vcs.xml, and 0 surviving processes.
Assisted-by: Claude Opus 5
2026-08-19 08:11:12 -04:00
|
|
|
script.index("pycharm_configuration.py"),
|
[FIX] script todo: ouvrir PyCharm après l'installation, pas avant
« ⚠ pas de .idea » à chaque installation : PyCharm ouvrait un dépôt cloné mais
pas installé, son configurateur d'interpréteur Python échouait faute de venv, et
il renonçait avant d'écrire quoi que ce soit. Le même appel sur un dépôt
installé écrit erplibre.iml, misc.xml, modules.xml et vcs.xml en cinq minutes —
mesuré sur la VM.
L'ouverture passe donc après le make, et « make pycharm_configure » la suit :
pycharm_update() s'était déjà exécuté pendant l'installation, quand il n'y avait
rien à configurer. Le groupe rend toujours 0 — un bonus ne rougit pas une VM —
et la phase mobile, qui porte le verdict, reste après lui.
--- EN ---
"⚠ no .idea" on every install: PyCharm was opening a repo that was cloned but
not installed, its Python interpreter configurator failed for lack of a venv,
and it gave up before writing anything. The same call on an installed repo
writes erplibre.iml, misc.xml, modules.xml and vcs.xml in five minutes —
measured on the VM.
The open therefore moves after the make, with "make pycharm_configure" behind
it: pycharm_update() had already run during the install, when there was nothing
to configure. The group always returns 0 — a bonus does not redden a VM — and
the mobile phase, which carries the verdict, still comes after it.
Assisted-by: Claude Opus 5
2026-08-19 07:50:19 -04:00
|
|
|
script.index("ERPLibre mobile"),
|
[FIX] todo qemu: open the PyCharm project without a screen, and unlicensed
Two measurements, one after the other. The unified PyCharm that
code=PCC&latest now serves stops on its licence: the log says
NoValidIdeLicense then "Get licenses: request requires authentication", and
no project is ever opened — so no .idea, so nothing for the install to
configure. The Community line asks for no account and is still patched
(2025.2.6.2 on 2026-07-29); it is resolved from the release feed, so no
version is frozen here.
That build then froze 1.3 s after startup: the trust dialog, invisible under
Xvfb and waiting for a click. idea.trust.all.projects unblocks it. Checked
on an Ubuntu 26.04 VM: .idea complete in 195 s, and pycharm_configuration.py
writes its exclusions into erplibre.iml.
--- FR ---
Deux mesures, l'une après l'autre. Le PyCharm unifié que sert désormais
code=PCC&latest s'arrête sur sa licence : le journal dit NoValidIdeLicense
puis « Get licenses: request requires authentication », et aucun projet ne
s'ouvre — donc pas de .idea, donc rien à configurer pour l'installation. La
ligne Community ne demande aucun compte et reste corrigée (2025.2.6.2 le
2026-07-29) ; elle est résolue depuis le flux des versions, sans qu'aucun
numéro ne soit figé ici.
Ce build se figeait ensuite 1,3 s après le démarrage : la fenêtre de
confiance, invisible sous Xvfb et attendant un clic.
idea.trust.all.projects la lève. Vérifié sur une VM Ubuntu 26.04 : .idea
complet en 195 s, et pycharm_configuration.py écrit ses exclusions dans
erplibre.iml.
Assisted-by: Claude Opus 5
2026-08-17 23:22:38 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
def test_the_ide_gets_its_own_process_group(self):
|
|
|
|
|
"""Sans « setsid », il n'y a pas de groupe à tuer."""
|
|
|
|
|
self.assertIn("setsid xvfb-run", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_the_whole_group_is_killed_not_just_the_wrapper(self):
|
|
|
|
|
"""Le signe moins est tout le correctif : « -$pid » désigne le GROUPE,
|
|
|
|
|
donc xvfb-run, Xvfb, pycharm et les cef_server."""
|
|
|
|
|
self.assertIn("kill -TERM -$pid", self.cmd)
|
|
|
|
|
self.assertIn("kill -KILL -$pid", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_inotify_is_raised_before_opening(self):
|
|
|
|
|
"""Après l'ouverture, il serait trop tard : l'analyse a déjà échoué."""
|
|
|
|
|
pos_watch = self.cmd.index("max_user_watches")
|
|
|
|
|
pos_open = self.cmd.index("setsid xvfb-run")
|
|
|
|
|
self.assertLess(pos_watch, pos_open)
|
|
|
|
|
self.assertIn("524288", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_the_leftover_count_is_a_single_number(self):
|
|
|
|
|
"""« pgrep -fc » imprime 0 ET rend 1 quand il ne trouve rien : le
|
|
|
|
|
« || echo 0 » ajoutait un second zéro, et « 0\n0 » n'est pas « 0 ».
|
|
|
|
|
Le filet se déclenchait donc à chaque passage."""
|
|
|
|
|
self.assertIn("| wc -l", self.cmd)
|
|
|
|
|
self.assertNotIn("pgrep -fc", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_it_is_valid_shell(self):
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-n"], input=self.cmd, capture_output=True, text=True
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stderr)
|
|
|
|
|
|
|
|
|
|
def test_the_group_kill_really_reaps_the_children(self):
|
|
|
|
|
"""Le test qui compte : on rejoue l'étape avec de FAUX pycharm et
|
|
|
|
|
xvfb-run, celui-ci laissant un enfant derrière lui comme le vrai le
|
|
|
|
|
fait avec Xvfb. Rien ne doit survivre."""
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
bin_dir = pathlib.Path(tmp) / "bin"
|
|
|
|
|
bin_dir.mkdir()
|
|
|
|
|
repo = pathlib.Path(tmp) / "repo"
|
|
|
|
|
(repo / ".idea").mkdir(parents=True)
|
|
|
|
|
# .idea déjà là : l'attente sort au premier tour, et le test
|
|
|
|
|
# mesure la FERMETURE, pas la création.
|
|
|
|
|
(repo / ".idea" / "erplibre.iml").write_text("<module/>")
|
|
|
|
|
(repo / ".idea" / "misc.xml").write_text("<project/>")
|
|
|
|
|
marker = pathlib.Path(tmp) / "alive"
|
|
|
|
|
(bin_dir / "xvfb-run").write_text(
|
|
|
|
|
"#!/bin/bash\n"
|
|
|
|
|
# L'enfant qui survivait : un Xvfb que personne ne tuait.
|
|
|
|
|
f"( while true; do touch {marker}; sleep 1; done ) &\n"
|
|
|
|
|
'shift; exec "$@"\n'
|
|
|
|
|
)
|
|
|
|
|
(bin_dir / "pycharm").write_text(
|
|
|
|
|
"#!/bin/bash\nwhile true; do sleep 1; done\n"
|
|
|
|
|
)
|
|
|
|
|
(bin_dir / "sudo").write_text("#!/bin/bash\nexit 0\n")
|
|
|
|
|
(bin_dir / "python3").write_text("#!/bin/bash\ncat > /dev/null\n")
|
[FIX] script todo: viser l'IDE par son nom, pas par la ligne de commande
Le filet de fermeture de PyCharm cherchait « /opt/pycharm » dans les lignes de
commande. Or le script d'installation est passé en argument à ssh, et il
contient ce chemin : le pkill a tué la session ssh qui portait une installation
en cours. Elle est morte sans marqueur de sortie, et le tableau de bord a
montré un sablier pendant 54 minutes. Exécuter la suite de tests suffisait à
déclencher le défaut, puisqu'un test rejoue l'étape.
Le filet vise désormais les NOMS de processus, bornés au compte courant.
Mesuré dans la VM : par nom, 3 processus réels et aucun faux ; par ligne de
commande, 4 — le ssh compris. Un test plante un témoin nommé « sleep » dont la
ligne contient le chemin de l'IDE, et les tests bouchonnent pgrep et pkill.
--- EN ---
PyCharm's closing net looked for "/opt/pycharm" in command lines. But the
install script is passed to ssh as an argument, and it contains that path: the
pkill killed the ssh session carrying a running install. It died with no exit
marker, and the dashboard showed an hourglass for 54 minutes. Running the test
suite was enough to trigger it, since one test replays the step.
The net now targets process NAMES, scoped to the current account. Measured in
the VM: by name, 3 real processes and no false ones; by command line, 4 — the
ssh included. A test plants a witness named "sleep" whose command line holds
the IDE path, and the tests stub pgrep and pkill.
Assisted-by: Claude Opus 5
2026-08-19 07:26:54 -04:00
|
|
|
# pgrep et pkill sont BOUCHONNÉS, et c'est le point important : le
|
|
|
|
|
# filet de l'étape balaie les processus du compte courant. Exécuté
|
|
|
|
|
# sans bouchon sur la machine de développement, il fermerait le
|
|
|
|
|
# PyCharm de l'utilisateur. C'est le groupe qu'on teste ici, pas le
|
|
|
|
|
# filet — celui-ci est vérifié à part, sans rien tuer.
|
|
|
|
|
(bin_dir / "pgrep").write_text("#!/bin/bash\nexit 1\n")
|
|
|
|
|
(bin_dir / "pkill").write_text("#!/bin/bash\nexit 0\n")
|
|
|
|
|
for name in (
|
|
|
|
|
"xvfb-run",
|
|
|
|
|
"pycharm",
|
|
|
|
|
"sudo",
|
|
|
|
|
"python3",
|
|
|
|
|
"pgrep",
|
|
|
|
|
"pkill",
|
|
|
|
|
):
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
(bin_dir / name).chmod(0o755)
|
|
|
|
|
cmd = self.todo._qemu_pycharm_project_cmd(False).replace(
|
|
|
|
|
self.todo._qemu_install_dir(False), str(repo)
|
|
|
|
|
)
|
|
|
|
|
env = dict(os.environ, PATH=f"{bin_dir}:/usr/bin:/bin", HOME=tmp)
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-c", cmd],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
env=env,
|
|
|
|
|
timeout=180,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stdout + res.stderr)
|
|
|
|
|
marker.unlink(missing_ok=True)
|
|
|
|
|
time.sleep(3)
|
|
|
|
|
# L'enfant réveillait le marqueur chaque seconde : s'il vit
|
|
|
|
|
# encore, le fichier est revenu.
|
|
|
|
|
self.assertFalse(
|
|
|
|
|
marker.exists(),
|
|
|
|
|
"un enfant a survécu à la fermeture du groupe",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
[IMP] script todo: ne pas retélécharger un IDE déjà installé
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 : environ cinq
minutes pour PyCharm, autant pour Android Studio, à chaque fois. Les deux
étapes vérifient donc /opt avant de sortir curl.
Mesuré sur erplibre-ubuntu-2604-gnome, IDE déjà posés : les deux étapes passent
de dix minutes à 0,094 s au total. Le reste rejoue quand même — lanceur, alias,
raccourci de bureau — il est idempotent et bon marché. Un test vérifie que la
garde n'avale pas l'échec du cas où il faut bel et bien télécharger.
--- EN ---
Replaying an install is the normal case — one that died, a tool added later —
and the download is the long part: about five minutes for PyCharm, as much for
Android Studio, every time. Both steps now check /opt before reaching for curl.
Measured on erplibre-ubuntu-2604-gnome with both IDEs already in place: the two
steps drop from ten minutes to 0.094 s total. The rest still replays — launcher,
alias, desktop entry — it is idempotent and cheap. A test checks the guard does
not swallow the failure of the case where downloading is actually needed.
Assisted-by: Claude Opus 5
2026-08-19 07:35:26 -04:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
[FIX] script todo: viser l'IDE par son nom, pas par la ligne de commande
Le filet de fermeture de PyCharm cherchait « /opt/pycharm » dans les lignes de
commande. Or le script d'installation est passé en argument à ssh, et il
contient ce chemin : le pkill a tué la session ssh qui portait une installation
en cours. Elle est morte sans marqueur de sortie, et le tableau de bord a
montré un sablier pendant 54 minutes. Exécuter la suite de tests suffisait à
déclencher le défaut, puisqu'un test rejoue l'étape.
Le filet vise désormais les NOMS de processus, bornés au compte courant.
Mesuré dans la VM : par nom, 3 processus réels et aucun faux ; par ligne de
commande, 4 — le ssh compris. Un test plante un témoin nommé « sleep » dont la
ligne contient le chemin de l'IDE, et les tests bouchonnent pgrep et pkill.
--- EN ---
PyCharm's closing net looked for "/opt/pycharm" in command lines. But the
install script is passed to ssh as an argument, and it contains that path: the
pkill killed the ssh session carrying a running install. It died with no exit
marker, and the dashboard showed an hourglass for 54 minutes. Running the test
suite was enough to trigger it, since one test replays the step.
The net now targets process NAMES, scoped to the current account. Measured in
the VM: by name, 3 real processes and no false ones; by command line, 4 — the
ssh included. A test plants a witness named "sleep" whose command line holds
the IDE path, and the tests stub pgrep and pkill.
Assisted-by: Claude Opus 5
2026-08-19 07:26:54 -04:00
|
|
|
class TestPycharmNetIsNarrow(unittest.TestCase):
|
|
|
|
|
"""Le filet de fermeture ne doit JAMAIS viser le ssh qui porte l'install.
|
|
|
|
|
|
|
|
|
|
Vécu, et cher : le filet cherchait « /opt/pycharm » dans les LIGNES DE
|
|
|
|
|
COMMANDE. Or la commande d'installation est passée en argument à ssh, et
|
|
|
|
|
elle contient ce chemin — le pkill a donc tué la session ssh qui portait
|
|
|
|
|
l'installation en cours sur l'hyperviseur. Elle est morte en silence, sans
|
|
|
|
|
marqueur de sortie : 48 minutes perdues, et rien dans le journal.
|
|
|
|
|
|
|
|
|
|
Mesuré ensuite dans une VM : par NOM de processus, 3 processus réels
|
|
|
|
|
(pycharm, Xvfb, fsnotifier) et aucun faux ; par ligne de commande, 4 — le
|
|
|
|
|
ssh compris.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.cmd = self.todo._qemu_pycharm_project_cmd()
|
|
|
|
|
|
|
|
|
|
def test_it_matches_by_process_name(self):
|
|
|
|
|
self.assertIn('pgrep -u "$(id -u)" -x', self.cmd)
|
|
|
|
|
self.assertIn('pkill -u "$(id -u)" -x', self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_it_never_matches_by_command_line(self):
|
|
|
|
|
"""« -f » est exactement ce qui a tué l'installation."""
|
|
|
|
|
self.assertNotIn("pkill -f", self.cmd)
|
|
|
|
|
self.assertNotIn("pgrep -f", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_the_names_are_the_ones_measured_in_the_vm(self):
|
|
|
|
|
for name in ("pycharm", "cef_server", "fsnotifier", "Xvfb"):
|
|
|
|
|
self.assertIn(name, self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_a_command_line_that_merely_mentions_the_ide_is_spared(self):
|
|
|
|
|
"""Le test qui compte, et il ne tue rien : un témoin dont la LIGNE
|
|
|
|
|
contient le chemin de l'IDE — comme le ssh lanceur — et dont le NOM est
|
|
|
|
|
« sleep ». L'ancien motif l'attrape, le nouveau l'épargne."""
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
pattern = re.search(r'-x "([^"]+)"', self.cmd).group(1)
|
|
|
|
|
witness = subprocess.Popen(
|
|
|
|
|
[
|
|
|
|
|
"bash",
|
|
|
|
|
"-c",
|
|
|
|
|
'exec -a "ssh erplibre@vm bash -c /opt/pycharm/bin/pycharm.sh"'
|
|
|
|
|
" sleep 30",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
time.sleep(1.5)
|
|
|
|
|
uid = str(os.getuid())
|
|
|
|
|
by_name = subprocess.run(
|
|
|
|
|
["pgrep", "-u", uid, "-x", pattern],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
).stdout.split()
|
|
|
|
|
by_cmdline = subprocess.run(
|
|
|
|
|
["pgrep", "-u", uid, "-f", "[/]opt/pycharm"],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
).stdout.split()
|
|
|
|
|
pid = str(witness.pid)
|
|
|
|
|
# Le témoin est un enfant de bash : on cherche le groupe entier.
|
|
|
|
|
spared = pid not in by_name
|
|
|
|
|
self.assertTrue(spared, "le motif par nom a attrapé le témoin")
|
|
|
|
|
self.assertIn(
|
|
|
|
|
pid,
|
|
|
|
|
by_cmdline,
|
|
|
|
|
"le témoin devrait être attrapé par l'ancien motif ;"
|
|
|
|
|
" sinon ce test ne prouve rien",
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
witness.kill()
|
|
|
|
|
witness.wait(timeout=10)
|
|
|
|
|
|
|
|
|
|
|
[FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.
Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.
--- EN ---
On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.
The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.
Assisted-by: Claude Opus 5
2026-08-19 05:38:01 -04:00
|
|
|
class TestMobileSwap(unittest.TestCase):
|
|
|
|
|
"""Le swap posé avant de compiler, et son refus de bloquer.
|
|
|
|
|
|
|
|
|
|
Mesuré : le démon Gradle a atteint 6,8 Go de RSS hors tas — son -Xmx1536m
|
|
|
|
|
ne le borne pas — sur une VM de 12 Go SANS swap, et le noyau l'a tué deux
|
|
|
|
|
fois. « --max-workers=2 » n'a rien changé : le pic est passé de 10,3 à
|
|
|
|
|
11,2 Go. C'est de la marge qu'il faut."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.steps = self.todo._qemu_mobile_build_steps("/tmp/el")
|
|
|
|
|
|
|
|
|
|
def test_the_swap_comes_before_the_build(self):
|
|
|
|
|
self.assertLess(
|
|
|
|
|
self.steps.index("SwapTotal"), self.steps.index("npm ci")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_it_does_nothing_when_swap_is_already_there(self):
|
|
|
|
|
self.assertIn("SwapTotal", self.steps)
|
|
|
|
|
self.assertIn("-lt 2000000", self.steps)
|
|
|
|
|
|
|
|
|
|
def test_a_failed_swap_leaves_no_stray_file(self):
|
|
|
|
|
"""Un fichier d'échange à moitié fait occuperait 4 Go pour rien."""
|
|
|
|
|
self.assertIn("rm -f /swapfile-erplibre", self.steps)
|
|
|
|
|
|
|
|
|
|
def test_it_is_valid_shell(self):
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-n"],
|
|
|
|
|
input="mstep() { :; }\n" + self.steps,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stderr)
|
|
|
|
|
|
|
|
|
|
def test_the_swap_block_is_not_linked_by_and(self):
|
|
|
|
|
"""Lié par « && », un swap refusé arrêterait toute la compilation."""
|
|
|
|
|
head = self.steps[: self.steps.index("npm ci")]
|
|
|
|
|
self.assertNotIn("fi; fi && ", head)
|
|
|
|
|
self.assertIn("fi; fi; ", head)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMobileDiagMemory(unittest.TestCase):
|
|
|
|
|
"""« Son démon a disparu » ne parle pas de mémoire ; le diagnostic, oui."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.cmd = self.todo._qemu_mobile_diag_cmd()
|
|
|
|
|
|
|
|
|
|
def test_the_oom_pattern_names_memory(self):
|
|
|
|
|
pats = {p[0]: p[1] for p in TODO._QEMU_MOBILE_DIAG}
|
|
|
|
|
self.assertIn("daemon disappeared", pats)
|
|
|
|
|
self.assertIn("memory", pats["daemon disappeared"].lower())
|
|
|
|
|
|
|
|
|
|
def test_the_zip_entry_limit_is_named(self):
|
|
|
|
|
"""La panne d'aujourd'hui, et elle est en amont : un APK est un ZIP
|
|
|
|
|
borné à 65 535 entrées, et le dépôt mobile en embarque 122 684 sous
|
|
|
|
|
assets/public/repos pour 337 qui sont l'application. Le diagnostic doit
|
|
|
|
|
le dire, pas laisser lire 5 000 lignes de Gradle."""
|
|
|
|
|
pats = {e[0]: e[1] for e in TODO._QEMU_MOBILE_DIAG}
|
|
|
|
|
self.assertIn("Too many zip entries", pats)
|
|
|
|
|
self.assertIn("65535", pats["Too many zip entries"])
|
|
|
|
|
|
|
|
|
|
def test_the_zip_limit_is_named_before_the_generic_gradle_failure(self):
|
|
|
|
|
"""« FAILED » attrape tout : placé avant, il masquerait la vraie
|
|
|
|
|
cause — l'ordre du tableau est le diagnostic."""
|
|
|
|
|
keys = [e[0] for e in TODO._QEMU_MOBILE_DIAG]
|
|
|
|
|
self.assertLess(
|
|
|
|
|
keys.index("Too many zip entries"), keys.index("FAILED")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_the_cause_is_proven_not_assumed(self):
|
|
|
|
|
"""Le compte de l'oom-killer et la RAM viennent avec : une cause
|
|
|
|
|
« mémoire » sans chiffre serait une supposition de plus."""
|
|
|
|
|
self.assertIn("mmem()", self.cmd)
|
|
|
|
|
self.assertIn("MemTotal", self.cmd)
|
|
|
|
|
self.assertIn("oom-kill", self.cmd)
|
|
|
|
|
|
|
|
|
|
def test_it_runs_and_names_the_cause(self):
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
bin_dir = pathlib.Path(tmp) / "bin"
|
|
|
|
|
bin_dir.mkdir()
|
|
|
|
|
# Un dmesg qui rapporte un oom-kill, via un sudo neutre.
|
|
|
|
|
(bin_dir / "sudo").write_text('#!/bin/bash\nshift 0; exec "$@"\n')
|
|
|
|
|
(bin_dir / "dmesg").write_text(
|
|
|
|
|
"#!/bin/bash\necho 'oom-kill:constraint=CONSTRAINT_NONE'\n"
|
|
|
|
|
)
|
|
|
|
|
for name in ("sudo", "dmesg"):
|
|
|
|
|
(bin_dir / name).chmod(0o755)
|
|
|
|
|
log = pathlib.Path(tmp) / "build.log"
|
|
|
|
|
log.write_text(
|
|
|
|
|
"> Task :app:compressDebugAssets\n"
|
|
|
|
|
"Gradle build daemon disappeared unexpectedly\n"
|
|
|
|
|
)
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-c", self.cmd + f'mdiag "{log}"'],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
env=dict(os.environ, PATH=f"{bin_dir}:/usr/bin:/bin"),
|
|
|
|
|
timeout=60,
|
|
|
|
|
)
|
|
|
|
|
out = res.stdout
|
|
|
|
|
# « RAM » et « OOM » traversent les deux langues ; le chiffre,
|
|
|
|
|
# lui, est ce qui distingue une cause prouvée d'une supposition.
|
|
|
|
|
self.assertIn("RAM", out)
|
|
|
|
|
self.assertRegex(out, r"\(OOM\)|OOM kills")
|
|
|
|
|
self.assertRegex(out, r"[0-9]+")
|
|
|
|
|
|
[FIX] todo qemu: open the PyCharm project without a screen, and unlicensed
Two measurements, one after the other. The unified PyCharm that
code=PCC&latest now serves stops on its licence: the log says
NoValidIdeLicense then "Get licenses: request requires authentication", and
no project is ever opened — so no .idea, so nothing for the install to
configure. The Community line asks for no account and is still patched
(2025.2.6.2 on 2026-07-29); it is resolved from the release feed, so no
version is frozen here.
That build then froze 1.3 s after startup: the trust dialog, invisible under
Xvfb and waiting for a click. idea.trust.all.projects unblocks it. Checked
on an Ubuntu 26.04 VM: .idea complete in 195 s, and pycharm_configuration.py
writes its exclusions into erplibre.iml.
--- FR ---
Deux mesures, l'une après l'autre. Le PyCharm unifié que sert désormais
code=PCC&latest s'arrête sur sa licence : le journal dit NoValidIdeLicense
puis « Get licenses: request requires authentication », et aucun projet ne
s'ouvre — donc pas de .idea, donc rien à configurer pour l'installation. La
ligne Community ne demande aucun compte et reste corrigée (2025.2.6.2 le
2026-07-29) ; elle est résolue depuis le flux des versions, sans qu'aucun
numéro ne soit figé ici.
Ce build se figeait ensuite 1,3 s après le démarrage : la fenêtre de
confiance, invisible sous Xvfb et attendant un clic.
idea.trust.all.projects la lève. Vérifié sur une VM Ubuntu 26.04 : .idea
complet en 195 s, et pycharm_configuration.py écrit ses exclusions dans
erplibre.iml.
Assisted-by: Claude Opus 5
2026-08-17 23:22:38 -04:00
|
|
|
|
[ADD] script todo: lancer l'émulateur Android et son tunnel adb
Le tunnel n'était que du texte à recopier, et le démarrage restait manuel :
un second émulateur sur le même AVD tue le premier (« Running multiple
emulators »), rencontré deux fois. Le menu le détecte donc avant de lancer.
La fenêtre décide du reste. Sans elle, todo.py démarre à distance, détaché
par setsid dans le groupe kvm, et enchaîne sur scrcpy ; avec elle, la
commande doit partir du poste qui possède l'affichage, pas d'ici.
Mesuré : l'émulateur n'écoute que sur le 127.0.0.1 de la VM — l'hyperviseur
est refusé sur IP:5555 — d'où le -J qui met la VM en dernier saut. Vérifié
sur erplibre-mobile-proof : poignée de main adb CNXN rendant
sdk_gphone64_x86_64, enveloppe détachée survivant au ssh ; 30 tests.
--- EN ---
The tunnel was only text to copy, and starting the emulator stayed manual:
a second emulator on the same AVD kills the first ("Running multiple
emulators"), hit twice. The menu now detects it before starting.
The window decides the rest. Without one, todo.py starts it remotely,
detached by setsid in the kvm group, and chains into scrcpy; with one, the
command must run from the workstation that owns the display, not from here.
Measured: the emulator only listens on the VM's 127.0.0.1 — the hypervisor
is refused on IP:5555 — hence the -J putting the VM last. Verified on
erplibre-mobile-proof: an adb CNXN handshake returning sdk_gphone64_x86_64,
a detached wrapper outliving the ssh; 30 tests.
Assisted-by: Claude Opus 5
2026-08-19 03:35:31 -04:00
|
|
|
class TestAvdStep(unittest.TestCase):
|
|
|
|
|
"""L'étape AVD, et ce qu'elle laisse comme piste dans le journal.
|
|
|
|
|
|
|
|
|
|
Le tunnel adb lui-même est vérifié dans test_qemu_emulator_menu.py, qui
|
|
|
|
|
couvre aussi le démarrage sans fenêtre et la question de la fenêtre.
|
|
|
|
|
"""
|
[ADD] todo qemu: offer the adb tunnel for scrcpy, X11-free
X11 carries raw pixels: 0.62 megapixel per frame even after shrinking the
screen, rendered in software. scrcpy receives an H.264 stream encoded BY the
device, so the emulator runs with no window at all — no X11 anywhere. The
tunnel menu gained a fourth kind, reusing the target list it already reads from
~/.ssh/config, ProxyJump included: the hard part was already solved there.
It forwards 5555, the emulator's own adb port, not 5037: tunnelling the adb
server would force the user to kill the one on their workstation, which holds
the same port. Verified through the tunnel from the hypervisor: an adb CNXN
handshake answers device::ro.product.name=sdk_gphone64_x86, exactly what adb
connect does. ss -ltn in the VM confirms 5554, 5555 and 5037 all listen on
127.0.0.1.
--- FR ---
X11 transporte des pixels bruts : 0,62 mégapixel par image même après réduction
de l'écran, en rendu logiciel. scrcpy reçoit un flux H.264 encodé PAR
l'appareil, si bien que l'émulateur tourne sans aucune fenêtre — plus de X11
nulle part. Le menu de tunnel gagne un quatrième type, qui réutilise la liste de
cibles qu'il lit déjà dans ~/.ssh/config, ProxyJump compris : le plus dur y
était déjà résolu.
Il redirige 5555, le port adb de l'émulateur lui-même, et non 5037 : tunneliser
le serveur adb obligerait à tuer celui du poste, qui occupe le même port.
Vérifié à travers le tunnel depuis l'hyperviseur : une poignée de main adb CNXN
répond device::ro.product.name=sdk_gphone64_x86, ce que fait exactement adb
connect. « ss -ltn » dans la VM confirme que 5554, 5555 et 5037 écoutent tous
sur 127.0.0.1.
Assisted-by: Claude Opus 5
2026-08-19 02:39:10 -04:00
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
|
[ADD] script todo: lancer l'émulateur Android et son tunnel adb
Le tunnel n'était que du texte à recopier, et le démarrage restait manuel :
un second émulateur sur le même AVD tue le premier (« Running multiple
emulators »), rencontré deux fois. Le menu le détecte donc avant de lancer.
La fenêtre décide du reste. Sans elle, todo.py démarre à distance, détaché
par setsid dans le groupe kvm, et enchaîne sur scrcpy ; avec elle, la
commande doit partir du poste qui possède l'affichage, pas d'ici.
Mesuré : l'émulateur n'écoute que sur le 127.0.0.1 de la VM — l'hyperviseur
est refusé sur IP:5555 — d'où le -J qui met la VM en dernier saut. Vérifié
sur erplibre-mobile-proof : poignée de main adb CNXN rendant
sdk_gphone64_x86_64, enveloppe détachée survivant au ssh ; 30 tests.
--- EN ---
The tunnel was only text to copy, and starting the emulator stayed manual:
a second emulator on the same AVD kills the first ("Running multiple
emulators"), hit twice. The menu now detects it before starting.
The window decides the rest. Without one, todo.py starts it remotely,
detached by setsid in the kvm group, and chains into scrcpy; with one, the
command must run from the workstation that owns the display, not from here.
Measured: the emulator only listens on the VM's 127.0.0.1 — the hypervisor
is refused on IP:5555 — hence the -J putting the VM last. Verified on
erplibre-mobile-proof: an adb CNXN handshake returning sdk_gphone64_x86_64,
a detached wrapper outliving the ssh; 30 tests.
Assisted-by: Claude Opus 5
2026-08-19 03:35:31 -04:00
|
|
|
def test_the_avd_step_points_at_the_tunnel(self):
|
[ADD] todo qemu: offer the adb tunnel for scrcpy, X11-free
X11 carries raw pixels: 0.62 megapixel per frame even after shrinking the
screen, rendered in software. scrcpy receives an H.264 stream encoded BY the
device, so the emulator runs with no window at all — no X11 anywhere. The
tunnel menu gained a fourth kind, reusing the target list it already reads from
~/.ssh/config, ProxyJump included: the hard part was already solved there.
It forwards 5555, the emulator's own adb port, not 5037: tunnelling the adb
server would force the user to kill the one on their workstation, which holds
the same port. Verified through the tunnel from the hypervisor: an adb CNXN
handshake answers device::ro.product.name=sdk_gphone64_x86, exactly what adb
connect does. ss -ltn in the VM confirms 5554, 5555 and 5037 all listen on
127.0.0.1.
--- FR ---
X11 transporte des pixels bruts : 0,62 mégapixel par image même après réduction
de l'écran, en rendu logiciel. scrcpy reçoit un flux H.264 encodé PAR
l'appareil, si bien que l'émulateur tourne sans aucune fenêtre — plus de X11
nulle part. Le menu de tunnel gagne un quatrième type, qui réutilise la liste de
cibles qu'il lit déjà dans ~/.ssh/config, ProxyJump compris : le plus dur y
était déjà résolu.
Il redirige 5555, le port adb de l'émulateur lui-même, et non 5037 : tunneliser
le serveur adb obligerait à tuer celui du poste, qui occupe le même port.
Vérifié à travers le tunnel depuis l'hyperviseur : une poignée de main adb CNXN
répond device::ro.product.name=sdk_gphone64_x86, ce que fait exactement adb
connect. « ss -ltn » dans la VM confirme que 5554, 5555 et 5037 écoutent tous
sur 127.0.0.1.
Assisted-by: Claude Opus 5
2026-08-19 02:39:10 -04:00
|
|
|
"""La question vient juste après « c'est trop lent » : la réponse doit
|
|
|
|
|
être à portée de journal."""
|
|
|
|
|
self.assertIn("tunnel > 4", self.todo._qemu_avd_remote_cmd())
|
|
|
|
|
|
|
|
|
|
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
class TestGnomeSiteExtensions(unittest.TestCase):
|
|
|
|
|
"""Extensions posées depuis extensions.gnome.org, par leur UUID."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.block = self.todo._qemu_gnome_ext_remote_cmd()
|
|
|
|
|
|
|
|
|
|
def test_the_three_requested_extensions_are_there(self):
|
|
|
|
|
for uuid in (
|
|
|
|
|
"gTile@vibou",
|
|
|
|
|
"freon@UshakovVasilii_Github.yahoo.com",
|
|
|
|
|
"tracker@aliakseiz.github.com",
|
|
|
|
|
):
|
|
|
|
|
self.assertIn(uuid, self.block, uuid)
|
|
|
|
|
|
|
|
|
|
def test_the_archive_follows_the_running_gnome(self):
|
|
|
|
|
"""Le site sert une archive DIFFERENTE selon la version demandée —
|
|
|
|
|
gTile v59 en GNOME 46, v62 en 48 : figer une URL poserait une archive
|
|
|
|
|
faite pour une autre version."""
|
|
|
|
|
self.assertIn("shell_version=$sv", self.block)
|
|
|
|
|
self.assertIn("gnome-shell --version", self.block)
|
|
|
|
|
|
|
|
|
|
def test_valid_shell_and_never_fails_the_install(self):
|
|
|
|
|
script = "set -e\n" + self.block
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
0,
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["bash", "-n"], input=script, text=True, capture_output=True
|
|
|
|
|
).returncode,
|
|
|
|
|
)
|
|
|
|
|
# Sans gnome-shell dans le PATH, le bloc doit se taire proprement.
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-c", "PATH=/nonexistent; " + script],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stderr)
|
|
|
|
|
|
|
|
|
|
def test_a_session_bus_is_provided_for_dconf(self):
|
|
|
|
|
"""Un « ssh hote commande » n'a pas de bus de session : sans lui,
|
|
|
|
|
l'activation ne peut rien ecrire dans dconf."""
|
|
|
|
|
self.assertIn("dbus-run-session", self.block)
|
|
|
|
|
|
[FIX] qemu install : poser mise, pyenv et starship, verdict par outil
Without pipefail, « curl … | sh » returns the status of sh, 0 on empty input:
a failed download passed for an install, its fallback never ran, and the
failure surfaced later as « command not found ». mise and pyenv are downloaded
into a mktemp file, then run, and a check refuses any fallback behind a pipe
without pipefail. starship's installer runs as root under a root timeout, never
reaching the « sudo -v » that sudo-rs refuses, and its shell hook is guarded.
Each tool then reports the version found or « not installed », without
tripping « set -e ».
--- FR ---
Sans pipefail, « curl … | sh » rend le statut de sh, 0 sur une entrée vide :
un téléchargement raté passait pour une pose, son repli ne tournait jamais, et
l'échec se lisait plus loin sous « command not found ». mise et pyenv sont
téléchargés dans un fichier tiré par mktemp, puis exécutés, et un contrôle
refuse tout repli derrière un tube sans pipefail. L'installateur de starship
tourne en root sous un délai root, sans atteindre le « sudo -v » que sudo-rs
refuse, et son crochet de shell est gardé. Chaque outil dit ensuite la version
trouvée ou « non installé », sans faire tomber « set -e ».
Assisted-by: Claude Opus 5
2026-09-14 14:41:20 -04:00
|
|
|
def _lancer(self, **env_en_plus):
|
|
|
|
|
"""Le bloc, pour de vrai, sous « set -e », dans un PATH où
|
|
|
|
|
gnome-shell, curl, mktemp et gnome-extensions sont faux : ni
|
|
|
|
|
réseau, ni session, ni fichier hors du répertoire du test."""
|
|
|
|
|
import os
|
|
|
|
|
import shutil
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
with mock.patch("script.todo.qemu_install.t", lambda k: k):
|
|
|
|
|
bloc = TODO.__new__(TODO)._qemu_gnome_ext_remote_cmd()
|
|
|
|
|
tmp = tempfile.TemporaryDirectory()
|
|
|
|
|
self.addCleanup(tmp.cleanup)
|
|
|
|
|
racine = pathlib.Path(tmp.name)
|
|
|
|
|
self.racine = racine
|
|
|
|
|
faux = racine / "bin"
|
|
|
|
|
faux.mkdir()
|
|
|
|
|
for outil in ("sh", "bash", "awk", "cut", "rm"):
|
|
|
|
|
os.symlink(shutil.which(outil), faux / outil)
|
|
|
|
|
corps = {
|
|
|
|
|
"gnome-shell": 'echo "GNOME Shell 50.1"\n',
|
|
|
|
|
"mktemp": 'if [ -n "$MKTEMP_ECHEC" ]; then exit 1; fi\n'
|
|
|
|
|
'f="$TMPDIR/gext.$$"; : > "$f"; echo "$f"\n',
|
|
|
|
|
"curl": 'echo "$*" >> "$HOME/curl.trace"\n'
|
|
|
|
|
'if [ -n "$ECHEC" ]; then exit 22; fi\n'
|
|
|
|
|
'while [ "$#" -gt 0 ]; do [ "$1" = -o ] && dest="$2"; shift;'
|
|
|
|
|
' done\n: > "$dest"\n',
|
|
|
|
|
"gnome-extensions": '[ "$1" = install ] && exit "${GX_RC:-0}"\n'
|
|
|
|
|
"exit 0\n",
|
|
|
|
|
}
|
|
|
|
|
for nom, texte in corps.items():
|
|
|
|
|
chemin = faux / nom
|
|
|
|
|
chemin.write_text("#!/bin/sh\n" + texte, encoding="utf-8")
|
|
|
|
|
chemin.chmod(0o755)
|
|
|
|
|
env = {
|
|
|
|
|
"PATH": str(faux),
|
|
|
|
|
"HOME": str(racine),
|
|
|
|
|
"TMPDIR": str(racine),
|
|
|
|
|
**env_en_plus,
|
|
|
|
|
}
|
|
|
|
|
fini = subprocess.run(
|
|
|
|
|
[str(faux / "bash"), "-c", "set -e\n" + bloc + "\necho FIN"],
|
|
|
|
|
env=env,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
timeout=60,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, fini.returncode, fini.stderr[-400:])
|
|
|
|
|
self.assertIn("FIN", fini.stdout)
|
|
|
|
|
return fini.stdout
|
|
|
|
|
|
|
|
|
|
def test_a_failed_download_does_not_blame_gnome(self):
|
|
|
|
|
"""Le site sert une archive même à un GNOME qu'il ne connaît pas :
|
|
|
|
|
un téléchargement raté ne dit rien de la version."""
|
|
|
|
|
sortie = self._lancer(ECHEC="1")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
len(TODO._QEMU_GNOME_EXT_UUIDS),
|
|
|
|
|
sortie.count("⚠ download impossible (network or cache):"),
|
|
|
|
|
)
|
|
|
|
|
self.assertNotIn("not available for this GNOME", sortie)
|
|
|
|
|
|
|
|
|
|
def test_a_failed_mktemp_skips_like_a_failed_download(self):
|
|
|
|
|
"""Sans nom tiré par mktemp, rien n'est téléchargé : un nom fixe dans
|
|
|
|
|
/var/tmp, ouvert à tous, pourrait y être posé d'avance."""
|
|
|
|
|
sortie = self._lancer(MKTEMP_ECHEC="1")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
len(TODO._QEMU_GNOME_EXT_UUIDS),
|
|
|
|
|
sortie.count("⚠ download impossible (network or cache):"),
|
|
|
|
|
)
|
|
|
|
|
self.assertNotIn("installed and enabled:", sortie)
|
|
|
|
|
self.assertFalse(
|
|
|
|
|
(self.racine / "curl.trace").exists(), "curl lancé sans fichier"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_no_fixed_path_in_the_shared_tmp(self):
|
|
|
|
|
"""/var/tmp n'apparaît que comme répertoire donné à mktemp, et le
|
|
|
|
|
bloc reste valide sous une traduction à apostrophe impaire."""
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
piege = "l'extension n'a pas « fini » aujourd'hui"
|
|
|
|
|
with mock.patch("script.todo.qemu_install.t", lambda k: piege):
|
|
|
|
|
bloc = TODO.__new__(TODO)._qemu_gnome_ext_remote_cmd()
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
{"/var/tmp"}, set(re.findall(r"/var/tmp[^\s;\"')|]*", bloc))
|
|
|
|
|
)
|
|
|
|
|
self.assertIn("mktemp -p /var/tmp gext-XXXX.zip", bloc)
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-n"], input=bloc, text=True, capture_output=True
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stderr[:400])
|
|
|
|
|
|
|
|
|
|
def test_a_refused_install_names_the_gnome_version(self):
|
|
|
|
|
sortie = self._lancer(GX_RC="1")
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
len(TODO._QEMU_GNOME_EXT_UUIDS),
|
|
|
|
|
sortie.count("not available for this GNOME, skipped:"),
|
|
|
|
|
)
|
|
|
|
|
self.assertIn("(GNOME 50)", sortie)
|
|
|
|
|
self.assertNotIn("download impossible", sortie)
|
|
|
|
|
|
|
|
|
|
def test_log_out_is_asked_only_when_something_was_installed(self):
|
|
|
|
|
"""Se reconnecter pour charger des extensions jamais posées est
|
|
|
|
|
une consigne qui ment."""
|
|
|
|
|
for echec in ({"ECHEC": "1"}, {"GX_RC": "1"}):
|
|
|
|
|
with self.subTest(echec=echec):
|
|
|
|
|
sortie = self._lancer(**echec)
|
|
|
|
|
self.assertNotIn("log out and back in", sortie)
|
|
|
|
|
sortie = self._lancer()
|
|
|
|
|
self.assertEqual(
|
|
|
|
|
len(TODO._QEMU_GNOME_EXT_UUIDS),
|
|
|
|
|
sortie.count("installed and enabled:"),
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(1, sortie.count("log out and back in to load them"))
|
|
|
|
|
|
|
|
|
|
def test_a_translated_apostrophe_keeps_the_block_valid(self):
|
|
|
|
|
"""Une apostrophe mal placée casse la commande distante ENTIÈRE. On
|
|
|
|
|
remplace la traduction — « set_lang » la persisterait.
|
|
|
|
|
|
|
|
|
|
Un nombre IMPAIR d'apostrophes : entre apostrophes, un nombre pair
|
|
|
|
|
se referme de lui-même, et « bash -n » ne verrait rien."""
|
|
|
|
|
piege = "l'extension n'a pas « fini » aujourd'hui"
|
|
|
|
|
with mock.patch("script.todo.qemu_install.t", lambda k: piege):
|
|
|
|
|
bloc = TODO.__new__(TODO)._qemu_gnome_ext_remote_cmd()
|
|
|
|
|
self.assertIn(piege, bloc)
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-n"], input=bloc, text=True, capture_output=True
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, res.returncode, res.stderr[:400])
|
|
|
|
|
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
|
2026-09-14 14:41:19 -04:00
|
|
|
class TestLeServiceDeLAgentInvite(unittest.TestCase):
|
|
|
|
|
"""Le service détaché qui pose qemu-guest-agent, lancé par cloud-init.
|
|
|
|
|
|
|
|
|
|
Son script essaie un gestionnaire de paquets après l'autre. Sans
|
|
|
|
|
accolades autour de chaque branche, une pose réussie enchaîne sur le
|
|
|
|
|
gestionnaire suivant, absent, et le service finit en échec — 127 sous
|
|
|
|
|
dash — alors que l'agent est posé. Le script est extrait de la vraie
|
|
|
|
|
configuration cloud-init, lue en YAML, et tourne dans un PATH où seul le
|
|
|
|
|
gestionnaire choisi existe, faux : aucun paquet n'est posé, aucun
|
|
|
|
|
service lancé.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def _script(self):
|
|
|
|
|
import shlex
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
from script.qemu import deploy_qemu
|
|
|
|
|
|
|
|
|
|
args = deploy_qemu.build_parser().parse_args(
|
|
|
|
|
["--distro", "ubuntu", "--hostname", "vm"]
|
|
|
|
|
)
|
|
|
|
|
doc = yaml.safe_load(deploy_qemu.build_cloud_config(args, None, []))
|
|
|
|
|
ligne = next(
|
|
|
|
|
c
|
|
|
|
|
for c in doc["runcmd"]
|
|
|
|
|
if isinstance(c, str) and "--unit=erplibre-qga" in c
|
|
|
|
|
)
|
|
|
|
|
mots = shlex.split(ligne)
|
|
|
|
|
# « systemd-run … /bin/sh -c '<script>' » : le script suit « -c ».
|
|
|
|
|
return mots[mots.index("/bin/sh") + 2]
|
|
|
|
|
|
|
|
|
|
def test_a_successful_install_exits_zero(self):
|
|
|
|
|
import os
|
|
|
|
|
import shutil
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
script = self._script()
|
|
|
|
|
for gestionnaire in ("apt-get", "dnf", "pacman"):
|
|
|
|
|
with self.subTest(gestionnaire=gestionnaire):
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
faux = pathlib.Path(tmp)
|
|
|
|
|
os.symlink(shutil.which("sh"), faux / "sh")
|
|
|
|
|
trace = faux / "trace"
|
|
|
|
|
chemin = faux / gestionnaire
|
|
|
|
|
chemin.write_text(
|
|
|
|
|
f'#!/bin/sh\necho "$0 $*" >> "{trace}"\nexit 0\n',
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
chemin.chmod(0o755)
|
|
|
|
|
fini = subprocess.run(
|
|
|
|
|
[str(faux / "sh"), "-c", script],
|
|
|
|
|
env={"PATH": str(faux)},
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
timeout=30,
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(0, fini.returncode, fini.stderr)
|
|
|
|
|
self.assertIn("qemu-guest-agent", trace.read_text())
|
|
|
|
|
|
|
|
|
|
|
2026-09-14 14:41:19 -04:00
|
|
|
class TestLeVerrouAptNeCoutePasDesMinutes(unittest.TestCase):
|
|
|
|
|
"""« Impossible d'obtenir le verrou /var/lib/apt/lists/lock. Il est
|
|
|
|
|
occupé par le processus N (apt-get) », répété pendant des minutes au
|
|
|
|
|
début de chaque installation de bureau.
|
|
|
|
|
|
|
|
|
|
Deux causes, et la première est la vraie : « disable --now » arrête un
|
|
|
|
|
MINUTEUR mais n'interrompt pas l'apt-get qu'il a déjà lancé, lequel garde
|
|
|
|
|
le verrou jusqu'au bout de sa mise à jour.
|
|
|
|
|
|
|
|
|
|
La seconde est le rythme : « DPkg::Lock::Timeout » ne couvre pas ce
|
|
|
|
|
verrou-là — il ne vaut que pour celui de dpkg — si bien qu'apt rend la
|
|
|
|
|
main en moins d'une seconde, et qu'un sommeil de dix secondes entre deux
|
|
|
|
|
essais est du temps payé pour rien.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.todo = TODO.__new__(TODO)
|
|
|
|
|
self.desarme = self.todo._qemu_no_auto_upgrade(prod=False)
|
|
|
|
|
self.cmd = self.todo._qemu_desktop_remote_cmd("gnome", "deb")
|
|
|
|
|
|
|
|
|
|
def test_le_service_deja_lance_est_arrete_lui_aussi(self):
|
|
|
|
|
"""C'est lui qui tient le verrou, pas le minuteur."""
|
|
|
|
|
self.assertIn("stop apt-daily.service", self.desarme)
|
|
|
|
|
self.assertIn("apt-daily-upgrade.service", self.desarme)
|
|
|
|
|
|
|
|
|
|
def test_les_minuteurs_restent_desarmes(self):
|
|
|
|
|
"""Les arrêter sans les désactiver les laisserait repartir en
|
|
|
|
|
pleine installation."""
|
|
|
|
|
self.assertIn("apt-daily.timer", self.desarme)
|
|
|
|
|
self.assertIn("apt-daily-upgrade.timer", self.desarme)
|
|
|
|
|
|
|
|
|
|
def test_en_production_on_ne_touche_a_rien(self):
|
|
|
|
|
"""Les correctifs de sécurité automatiques doivent rester actifs."""
|
|
|
|
|
self.assertEqual(self.todo._qemu_no_auto_upgrade(prod=True), "")
|
|
|
|
|
|
|
|
|
|
def test_la_boucle_repasse_souvent(self):
|
|
|
|
|
"""Un essai coûte moins d'une seconde : dormir dix secondes entre
|
|
|
|
|
deux multiplie par cinq l'attente d'un verrou qui se libère."""
|
|
|
|
|
i = self.cmd.index("until sudo apt-get")
|
|
|
|
|
boucle = self.cmd[i : self.cmd.index("done;", i)]
|
|
|
|
|
self.assertIn("sleep 2", boucle)
|
|
|
|
|
self.assertNotIn("sleep 10", boucle)
|
|
|
|
|
|
|
|
|
|
def test_lattente_couvre_notre_propre_service_detache(self):
|
|
|
|
|
"""Le vrai teneur du verrou, et c'est nous.
|
|
|
|
|
|
|
|
|
|
La pose de l'agent invité part en service DÉTACHÉ pour que
|
|
|
|
|
cloud-init rende la main en quelques secondes. Ce service fait un
|
|
|
|
|
« apt-get update » puis une installation : « cloud-init status
|
|
|
|
|
--wait » dit « done » pendant qu'il tient encore le verrou, et
|
|
|
|
|
l'étape suivante épuise ses reprises pour rien.
|
|
|
|
|
"""
|
|
|
|
|
attente = self.todo._qemu_cloud_init_wait()
|
|
|
|
|
self.assertIn("erplibre-qga", attente)
|
|
|
|
|
self.assertIn("is-active", attente)
|
|
|
|
|
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
def test_lattente_relit_les_variables_du_cache(self):
|
|
|
|
|
"""La session distante s'ouvre avant que cloud-init n'écrive les
|
|
|
|
|
variables du cache : sans les relire, un npm lancé sans sudo rejette
|
|
|
|
|
l'autorité du cache, alors que « sudo npm » l'accepte."""
|
|
|
|
|
from script.qemu.deploy_qemu import cache_env_reload
|
|
|
|
|
|
|
|
|
|
attente = self.todo._qemu_cloud_init_wait()
|
|
|
|
|
self.assertIn(cache_env_reload(), attente)
|
|
|
|
|
self.assertGreater(
|
|
|
|
|
attente.index(cache_env_reload()),
|
|
|
|
|
attente.index("status --wait"),
|
|
|
|
|
"les variables sont relues avant que cloud-init les ait écrites",
|
|
|
|
|
)
|
|
|
|
|
|
[FIX] déploiement qemu : attendre l'étape finale de cloud-init
« cloud-init status --wait » rend la main dès que cloud-init se déclare en
erreur — un module accessoire y suffit — alors que son étape finale écrit
encore l'autorité du cache, les variables et le fichier sudoers. Une session
ouverte dans cette seconde-là vit sans ces variables, PAM ne relisant plus le
fichier, et sudo n'a alors rien à conserver : l'installation lancée par sudo
rejette le certificat du cache. L'attente guette désormais l'unité
cloud-final, et son seul état « activating » : ce oneshot reste ACTIF une fois
fini, si bien qu'« is-active » y ferait attendre la borne entière pour rien.
--- EN ---
« cloud-init status --wait » returns as soon as cloud-init declares itself in
error — an accessory module is enough — while its final stage still writes the
cache authority, the variables and the sudoers file. A session opened in that
second lives without those variables, PAM never rereading the file, and sudo
then has nothing to keep: an install run by sudo rejects the cache
certificate. The wait now watches the cloud-final unit, and only its
« activating » state: this oneshot stays ACTIVE once finished, so « is-active »
would wait the whole bound for nothing.
Assisted-by: Claude Opus 5
2026-09-16 03:00:57 -04:00
|
|
|
def test_lattente_suit_l_unite_et_non_le_seul_statut(self):
|
|
|
|
|
"""cloud-init se déclare en erreur pour un module accessoire, et
|
|
|
|
|
« status --wait » rend alors la main pendant que son étape finale
|
|
|
|
|
écrit encore l'autorité du cache et les variables."""
|
|
|
|
|
from script.qemu.deploy_qemu import (
|
|
|
|
|
attente_cloud_final,
|
|
|
|
|
cache_env_reload,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
attente = self.todo._qemu_cloud_init_wait()
|
|
|
|
|
self.assertIn(attente_cloud_final(), attente)
|
|
|
|
|
self.assertLess(
|
|
|
|
|
attente.index(attente_cloud_final()),
|
|
|
|
|
attente.index(cache_env_reload()),
|
|
|
|
|
"les variables sont relues avant la fin de l'étape qui les écrit",
|
|
|
|
|
)
|
|
|
|
|
|
2026-09-14 14:41:19 -04:00
|
|
|
def test_le_nom_du_service_est_celui_que_le_deploiement_donne(self):
|
|
|
|
|
"""Deux noms qui divergent et l'attente ne trouve jamais rien."""
|
|
|
|
|
from script.qemu import deploy_qemu
|
|
|
|
|
|
|
|
|
|
src = pathlib.Path(deploy_qemu.__file__).read_text(encoding="utf-8")
|
|
|
|
|
self.assertIn("--unit=erplibre-qga", src)
|
|
|
|
|
|
|
|
|
|
def test_un_update_qui_nabouti_pas_le_dit(self):
|
|
|
|
|
"""Sans cette ligne, l'installation continue sur un index jamais
|
|
|
|
|
rafraîchi et échoue plus bas sur « Impossible de trouver le
|
|
|
|
|
paquet » — qui accuse le dépôt et non le verrou."""
|
|
|
|
|
i = self.cmd.index("until sudo apt-get")
|
|
|
|
|
boucle = self.cmd[i : self.cmd.index("done;", i)]
|
|
|
|
|
self.assertIn("echo", boucle)
|
|
|
|
|
self.assertIn("⚠", boucle)
|
|
|
|
|
|
|
|
|
|
def test_une_apostrophe_traduite_ne_casse_pas_la_commande(self):
|
|
|
|
|
"""Les messages sont traduits, et le français est plein
|
|
|
|
|
d'apostrophes. Une seule mal placée casse la commande distante
|
|
|
|
|
ENTIÈRE : la VM ne dit alors pas pourquoi elle n'a rien fait.
|
|
|
|
|
|
|
|
|
|
La langue n'est pas changée pour l'éprouver — « set_lang » la
|
|
|
|
|
PERSISTE dans env_var.sh, et un test qui la déplace fait échouer
|
|
|
|
|
tout ce qui suit. On remplace la traduction elle-même, le temps du
|
|
|
|
|
contrôle, par une chaîne qui porte le caractère dangereux.
|
[FIX] qemu install : poser mise, pyenv et starship, verdict par outil
Without pipefail, « curl … | sh » returns the status of sh, 0 on empty input:
a failed download passed for an install, its fallback never ran, and the
failure surfaced later as « command not found ». mise and pyenv are downloaded
into a mktemp file, then run, and a check refuses any fallback behind a pipe
without pipefail. starship's installer runs as root under a root timeout, never
reaching the « sudo -v » that sudo-rs refuses, and its shell hook is guarded.
Each tool then reports the version found or « not installed », without
tripping « set -e ».
--- FR ---
Sans pipefail, « curl … | sh » rend le statut de sh, 0 sur une entrée vide :
un téléchargement raté passait pour une pose, son repli ne tournait jamais, et
l'échec se lisait plus loin sous « command not found ». mise et pyenv sont
téléchargés dans un fichier tiré par mktemp, puis exécutés, et un contrôle
refuse tout repli derrière un tube sans pipefail. L'installateur de starship
tourne en root sous un délai root, sans atteindre le « sudo -v » que sudo-rs
refuse, et son crochet de shell est gardé. Chaque outil dit ensuite la version
trouvée ou « non installé », sans faire tomber « set -e ».
Assisted-by: Claude Opus 5
2026-09-14 14:41:20 -04:00
|
|
|
|
|
|
|
|
Le nombre d'apostrophes est IMPAIR : avec deux, un message emballé
|
|
|
|
|
entre guillemets simples se referme sur lui-même et le shell reste
|
|
|
|
|
valide, si bien que le contrôle ne verrait rien.
|
2026-09-14 14:41:19 -04:00
|
|
|
"""
|
|
|
|
|
import tempfile
|
|
|
|
|
|
[FIX] qemu install : poser mise, pyenv et starship, verdict par outil
Without pipefail, « curl … | sh » returns the status of sh, 0 on empty input:
a failed download passed for an install, its fallback never ran, and the
failure surfaced later as « command not found ». mise and pyenv are downloaded
into a mktemp file, then run, and a check refuses any fallback behind a pipe
without pipefail. starship's installer runs as root under a root timeout, never
reaching the « sudo -v » that sudo-rs refuses, and its shell hook is guarded.
Each tool then reports the version found or « not installed », without
tripping « set -e ».
--- FR ---
Sans pipefail, « curl … | sh » rend le statut de sh, 0 sur une entrée vide :
un téléchargement raté passait pour une pose, son repli ne tournait jamais, et
l'échec se lisait plus loin sous « command not found ». mise et pyenv sont
téléchargés dans un fichier tiré par mktemp, puis exécutés, et un contrôle
refuse tout repli derrière un tube sans pipefail. L'installateur de starship
tourne en root sous un délai root, sans atteindre le « sudo -v » que sudo-rs
refuse, et son crochet de shell est gardé. Chaque outil dit ensuite la version
trouvée ou « non installé », sans faire tomber « set -e ».
Assisted-by: Claude Opus 5
2026-09-14 14:41:20 -04:00
|
|
|
piege = "l'agent n'a pas fini aujourd'hui « attendre »"
|
2026-09-14 14:41:19 -04:00
|
|
|
with mock.patch("script.todo.qemu_install.t", lambda k: piege):
|
|
|
|
|
todo = TODO.__new__(TODO)
|
|
|
|
|
cmd = (
|
|
|
|
|
todo._qemu_cloud_init_wait()
|
|
|
|
|
+ todo._qemu_no_auto_upgrade(prod=False)
|
|
|
|
|
+ todo._qemu_desktop_remote_cmd("gnome", "deb")
|
|
|
|
|
)
|
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
|
|
|
"w", suffix=".sh", delete=False, encoding="utf-8"
|
|
|
|
|
) as fh:
|
|
|
|
|
fh.write(cmd)
|
|
|
|
|
chemin = fh.name
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
["bash", "-n", chemin], capture_output=True, text=True
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(res.returncode, 0, res.stderr[:400])
|
|
|
|
|
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
def test_la_boucle_est_bornee_par_le_temps(self):
|
|
|
|
|
"""La borne est une ÉCHÉANCE, pas un nombre d'essais.
|
|
|
|
|
|
|
|
|
|
Un essai coûte moins d'une seconde quand le verrou est tenu, et des
|
|
|
|
|
minutes quand le cache rend 504 sur chaque index : compter les essais
|
|
|
|
|
promettait cinq minutes et en valait des heures. Une échéance tient la
|
|
|
|
|
promesse quelle que soit la durée d'un essai.
|
|
|
|
|
"""
|
2026-09-14 14:41:19 -04:00
|
|
|
i = self.cmd.index("until sudo apt-get")
|
|
|
|
|
boucle = self.cmd[i : self.cmd.index("done;", i)]
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
self.assertIn("date +%s", boucle)
|
|
|
|
|
self.assertIn("-ge", boucle)
|
2026-09-14 14:41:19 -04:00
|
|
|
self.assertIn("break", boucle)
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
self.assertNotRegex(boucle, r"n=\$\(\(n\+1\)\)")
|
|
|
|
|
# L'échéance est POSÉE avant la boucle, sans quoi elle vaudrait zéro.
|
|
|
|
|
self.assertIn("fin=$(( $(date +%s) + 300 ))", self.cmd[:i])
|
2026-09-14 14:41:19 -04:00
|
|
|
|
|
|
|
|
|
[ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().
Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.
--- FR ---
Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().
Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.
Assisted-by: Claude Opus 5
2026-08-17 22:57:56 -04:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|