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
99 lines
3.6 KiB
Python
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()
|