erplibre/test/test_todo_launcher.py
Mathieu Benoit a8f7512130 [ADD] todo: todo.sh launcher, shown by make instead of install.sh
make prints the recipe it runs, and « ./install.sh » offered an installation
to read where one opens a menu. The launcher adds nothing but a name and
passes every argument to install.sh, which alone keeps picking an interpreter
able to READ the code and stays the entry point of a fresh machine.
A test caps todo.sh at six lines of code and refuses it python3, bin/python
and sort -V: two copies of that choice would drift. The changelog names the
launcher.
Checked: chain run through to the menu, exit 0; 9 tests.

--- FR ---

[ADD] todo : lanceur todo.sh, que make affiche à la place d'install.sh

make affiche la recette qu'il exécute, et « ./install.sh » y donnait à lire
une installation là où l'on ouvre un menu. Le lanceur n'ajoute qu'un nom et
passe chaque argument à install.sh, qui garde seul le choix d'un interpréteur
capable de LIRE le code et reste le point d'entrée d'une machine neuve.
Un test plafonne todo.sh à six lignes de code et lui refuse python3,
bin/python et sort -V : deux copies de ce choix dériveraient. Le changelog
nomme le lanceur.
Vérifié : chaîne lancée jusqu'au menu, sortie 0 ; 9 tests.

Assisted-by: Claude Opus 5
Assisted-by: Claude Opus 5.5
2026-09-25 01:29:53 -04:00

99 lines
3.6 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
"""La chaîne de lancement de TODO tient-elle en un seul endroit ?
« make todo » passe par todo.sh, qui passe par install.sh, qui choisit un
interpréteur capable de LIRE le code avant de le lui donner. Trois fichiers,
mais UNE seule décision : elle vit dans install.sh, qui est aussi le point
d'entrée d'une machine neuve.
Ce que ces tests gardent :
- todo.sh reste un NOM, et non une seconde copie du choix d'interpréteur.
Deux copies dérivent, et celle qui dérive est toujours celle qu'on ne lit
pas — ici, le chemin qu'emprunte « make » ;
- les arguments traversent. Un lanceur qui les avale rend « ./todo.sh » et
« ./install.sh » différents sans le dire ;
- todo.sh se place dans son propre répertoire : « make -C » et un appel par
chemin absolu partent d'ailleurs, et les chemins relatifs d'install.sh
n'y survivraient pas.
"""
import re
import subprocess
import unittest
from pathlib import Path
RACINE = Path(__file__).resolve().parents[1]
TODO_SH = RACINE / "todo.sh"
INSTALL_SH = RACINE / "install.sh"
MAKEFILE = (RACINE / "conf/make.todo.Makefile").read_text(encoding="utf-8")
SOURCE = TODO_SH.read_text(encoding="utf-8")
class TestCible(unittest.TestCase):
def test_make_todo_lance_todo_sh(self):
"""La recette s'affiche : elle doit nommer ce qu'elle fait."""
recette = re.search(r"^todo:\n\t(.+)$", MAKEFILE, re.MULTILINE)
self.assertIsNotNone(recette, MAKEFILE)
self.assertEqual("./todo.sh", recette.group(1).strip())
def test_make_n_rend_la_meme_chose(self):
sortie = subprocess.run(
["make", "-n", "todo"],
cwd=RACINE,
capture_output=True,
text=True,
)
self.assertEqual(0, sortie.returncode, sortie.stderr)
self.assertIn("./todo.sh", sortie.stdout)
class TestLanceur(unittest.TestCase):
def test_il_est_executable(self):
self.assertTrue(TODO_SH.exists())
self.assertTrue(TODO_SH.stat().st_mode & 0o111)
def test_il_delegue_a_install_sh(self):
self.assertTrue(INSTALL_SH.exists())
self.assertIn("./install.sh", SOURCE)
def test_les_arguments_traversent(self):
self.assertIn('"$@"', SOURCE)
def test_il_se_place_dans_son_repertoire(self):
self.assertIn('cd "$(dirname "$0")"', SOURCE)
def test_il_ne_choisit_pas_l_interprete_lui_meme(self):
"""Le choix vit dans install.sh. Le recopier ici en ferait deux, et
celle qui dérive serait celle que « make » emprunte."""
for marqueur in ("python3", "pyvenv.cfg", "sort -V", "bin/python"):
with self.subTest(marqueur=marqueur):
self.assertNotIn(marqueur, SOURCE)
def test_il_reste_court(self):
"""Au-delà d'une poignée de lignes, ce n'est plus un nom mais une
seconde implémentation."""
code = [
ligne
for ligne in SOURCE.splitlines()
if ligne.strip() and not ligne.lstrip().startswith("#")
]
self.assertLessEqual(len(code), 6, code)
class TestSyntaxe(unittest.TestCase):
def test_les_deux_scripts_se_lisent(self):
for script in (TODO_SH, INSTALL_SH):
with self.subTest(script=script.name):
sortie = subprocess.run(
["bash", "-n", str(script)],
capture_output=True,
text=True,
)
self.assertEqual(0, sortie.returncode, sortie.stderr)
if __name__ == "__main__":
unittest.main()