erplibre/test/test_todo_launcher.py

100 lines
3.6 KiB
Python
Raw Normal View History

#!/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()