145 lines
5.6 KiB
Python
145 lines
5.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)
|
||
|
|
"""Ce qu'est un nom de machine dans ~/.ssh/config, et pourquoi c'est partagé.
|
||
|
|
|
||
|
|
Trois lecteurs de ce fichier vivent dans ce dépôt, et ils tranchaient la même
|
||
|
|
question autrement : l'un comparait le mot-clé avec la casse, un autre
|
||
|
|
exigeait un espace là où une tabulation est légale, un troisième laissait
|
||
|
|
passer le motif nié. Une découverte voyait donc un alias que le menu de
|
||
|
|
montage ne voyait pas, ce qui se lit comme une panne intermittente et non
|
||
|
|
comme trois filtres différents.
|
||
|
|
|
||
|
|
Ces tests portent sur la décision elle-même, puis sur le fait que les trois
|
||
|
|
lecteurs la partagent — cette dernière assertion est ce qui empêche qu'un
|
||
|
|
quatrième filtre se réinstalle en douce.
|
||
|
|
|
||
|
|
Les alias sont inventés : `ssh_config` ne résout rien et ne touche aucun
|
||
|
|
fichier, donc aucun nom réel n'est nécessaire ici.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import unittest
|
||
|
|
|
||
|
|
from script.todo import ssh_config as sc
|
||
|
|
|
||
|
|
|
||
|
|
class TestCeQuiEstUneDeclaration(unittest.TestCase):
|
||
|
|
def test_a_plain_host_line(self):
|
||
|
|
self.assertEqual(sc.declared_names("Host alpha"), ["alpha"])
|
||
|
|
|
||
|
|
def test_lowercase_keyword(self):
|
||
|
|
"""ssh_config ne distingue pas la casse de ses mots-clés."""
|
||
|
|
self.assertEqual(sc.declared_names("host alpha"), ["alpha"])
|
||
|
|
|
||
|
|
def test_uppercase_keyword(self):
|
||
|
|
self.assertEqual(sc.declared_names("HOST alpha"), ["alpha"])
|
||
|
|
|
||
|
|
def test_tab_separator(self):
|
||
|
|
"""Une tabulation sépare aussi légalement qu'un espace."""
|
||
|
|
self.assertEqual(sc.declared_names("Host\talpha"), ["alpha"])
|
||
|
|
|
||
|
|
def test_indented_declaration(self):
|
||
|
|
self.assertEqual(sc.declared_names(" Host alpha"), ["alpha"])
|
||
|
|
|
||
|
|
def test_several_names_on_one_line(self):
|
||
|
|
self.assertEqual(
|
||
|
|
sc.declared_names("Host alpha beta gamma"),
|
||
|
|
["alpha", "beta", "gamma"],
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_hostname_is_not_a_declaration(self):
|
||
|
|
"""Le blanc exigé derrière le mot-clé est ce qui sépare les deux."""
|
||
|
|
self.assertIsNone(sc.declared_names(" HostName 10.83.4.19"))
|
||
|
|
|
||
|
|
def test_another_directive_is_not_a_declaration(self):
|
||
|
|
self.assertIsNone(sc.declared_names(" User quelquun"))
|
||
|
|
|
||
|
|
def test_an_empty_line_is_not_a_declaration(self):
|
||
|
|
self.assertIsNone(sc.declared_names(""))
|
||
|
|
self.assertIsNone(sc.declared_names(None))
|
||
|
|
|
||
|
|
|
||
|
|
class TestCeQuiNeDesigneAucuneMachine(unittest.TestCase):
|
||
|
|
def test_a_wildcard_is_a_rule(self):
|
||
|
|
self.assertEqual(sc.declared_names("Host *"), [])
|
||
|
|
|
||
|
|
def test_a_partial_wildcard_is_a_rule(self):
|
||
|
|
self.assertEqual(sc.declared_names("Host web-*"), [])
|
||
|
|
|
||
|
|
def test_a_single_char_wildcard_is_a_rule(self):
|
||
|
|
self.assertEqual(sc.declared_names("Host web-?"), [])
|
||
|
|
|
||
|
|
def test_a_negated_pattern_is_removed(self):
|
||
|
|
"""`!nom` RETIRE un nom : le retenir donne une cible en « ! »."""
|
||
|
|
self.assertEqual(sc.declared_names("Host alpha !beta"), ["alpha"])
|
||
|
|
|
||
|
|
def test_only_a_negation_declares_nothing(self):
|
||
|
|
self.assertEqual(sc.declared_names("Host !beta"), [])
|
||
|
|
|
||
|
|
def test_a_declaration_naming_nothing_is_still_a_declaration(self):
|
||
|
|
"""None et [] ne veulent pas dire la même chose.
|
||
|
|
|
||
|
|
`Host *` EST une déclaration : le lecteur qui accumule un bloc doit
|
||
|
|
clore le précédent, sinon les directives du bloc générique se
|
||
|
|
rattachent au bloc d'avant."""
|
||
|
|
self.assertIsNotNone(sc.declared_names("Host *"))
|
||
|
|
self.assertIsNone(sc.declared_names("Compression yes"))
|
||
|
|
|
||
|
|
|
||
|
|
class TestLesTroisLecteursPartagentLaDecision(unittest.TestCase):
|
||
|
|
"""Les assertions qui empêchent un quatrième filtre de se réinstaller.
|
||
|
|
|
||
|
|
Elles comptent les APPELS à la décision partagée, lus dans l'arbre
|
||
|
|
syntaxique : un lecteur qui recompile son propre motif cesse d'appeler,
|
||
|
|
et le compte tombe. Compter dans le texte accuserait le bon code, une
|
||
|
|
docstring qui nomme la fonction n'étant pas un appel."""
|
||
|
|
|
||
|
|
def _source(self, chemin):
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
racine = Path(__file__).resolve().parents[1]
|
||
|
|
return (racine / chemin).read_text(encoding="utf-8")
|
||
|
|
|
||
|
|
def _appels(self, chemin):
|
||
|
|
"""Le nombre d'APPELS à la décision partagée, lus dans l'arbre.
|
||
|
|
|
||
|
|
Compté sur l'arbre et non dans le texte : une docstring qui NOMME la
|
||
|
|
fonction n'est pas un appel, et une assertion sur le texte compterait
|
||
|
|
les deux."""
|
||
|
|
import ast
|
||
|
|
|
||
|
|
arbre = ast.parse(self._source(chemin))
|
||
|
|
return sum(
|
||
|
|
1
|
||
|
|
for n in ast.walk(arbre)
|
||
|
|
if isinstance(n, ast.Call)
|
||
|
|
and isinstance(n.func, ast.Attribute)
|
||
|
|
and n.func.attr == "declared_names"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_qemu_manage_delegates(self):
|
||
|
|
self.assertEqual(self._appels("script/todo/qemu_manage.py"), 1)
|
||
|
|
|
||
|
|
def test_todo_delegates_twice(self):
|
||
|
|
"""Les DEUX lecteurs de todo.py, pas seulement celui qu'on a corrigé."""
|
||
|
|
self.assertEqual(self._appels("script/todo/todo.py"), 2)
|
||
|
|
|
||
|
|
def test_parse_ssh_blocks_sees_a_lowercase_alias(self):
|
||
|
|
"""Le bout de bout : la forme qui échappait aux trois."""
|
||
|
|
from script.todo.qemu_manage import parse_ssh_blocks
|
||
|
|
|
||
|
|
blocs = parse_ssh_blocks("host alpha\n\tHostName 10.83.4.19\n")
|
||
|
|
self.assertIn("alpha", blocs)
|
||
|
|
self.assertEqual(blocs["alpha"]["hostname"], "10.83.4.19")
|
||
|
|
|
||
|
|
def test_parse_ssh_blocks_drops_a_negation(self):
|
||
|
|
from script.todo.qemu_manage import parse_ssh_blocks
|
||
|
|
|
||
|
|
blocs = parse_ssh_blocks("Host alpha !beta\n\tUser quelquun\n")
|
||
|
|
self.assertIn("alpha", blocs)
|
||
|
|
self.assertNotIn("!beta", blocs)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|