Set-OPS-Public/scripts/tests/test_sonder.py
Daniel Allaire 0595bf03c1
Some checks are pending
verifier / verifier (push) Waiting to run
sonder : rapporter ce qui distingue, au lieu d un echec
Trois faux diagnostics en une journee, tous dus a l instrument et aucun au
composant. curl et bash /dev/tcp ecrasent quatre causes incompatibles dans le meme
mot : ouvert, une POLITIQUE qui refuse, une machine ABSENTE, et la frontiere muette.

LE MEME CODE DIT DEUX CHOSES SELON LE DELAI, et c est la distinction qui a coute le
plus cher : EHOSTUNREACH immediat = pas de route ; le meme apres trois secondes =
il n y a pas de machine, c est l ARP qui renonce. Les confondre a fait appliquer un
pare-feu pour reparer un vide.  est pure, donc gardee par un test qui
exige que les deux ne se lisent jamais pareil.

LA GARDE ECHOUAIT DU COTE SILENCIEUX. L outil dit par quelle SOURCE le paquet part
et refuse de conclure sur une passerelle anycast — le piege qui m a fait declarer
muet un REJECT qui emettait bien ses RST. Ma premiere version rendait « pas
anycast » quand inventory_rules manquait, c est-a-dire sur un hyperviseur ou l on a
copie le seul fichier : precisement la ou le piege se produit. Une garde qui echoue
doit crier, pas se taire.

ET « NON CONCLUANT » EST UN RESULTAT : sur une source anycast et un silence, l outil
ne tranche pas, il dit quoi faire — compter les paquets du cote qui refuse, ou
sonder depuis une VM.

TCP seulement, et c est dit : UDP n a pas de poignee.

Valide contre le reel depuis ops-01 : trois cas sur quatre, le quatrieme attendant
deux machines vivantes dans un meme tenant.

make verifier : vert. make prouver : CONFORME, 53 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 17:02:20 -04:00

100 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""La sonde distingue-t-elle ce qu'un « echec » confond ?
CE QUE CES TESTS GARDENT, ET POURQUOI. Le 2026-08-28, trois faux diagnostics ont ete
poses en une journee — tous par l'instrument, aucun par le composant. Le pire des trois
n'etait pas une confusion entre « ouvert » et « ferme », mais entre DEUX FORMES DU MEME
CODE : `EHOSTUNREACH` immediat (pas de route) et `EHOSTUNREACH` apres trois secondes
(l'ARP qui renonce, donc AUCUNE MACHINE). Le second a fait appliquer un pare-feu pour
reparer un vide.
`interpreter` est une fonction PURE : elle se joue sans reseau, donc elle se garde ici.
"""
import errno
import sys
from pathlib import Path
RACINE = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(RACINE / "scripts"))
import sonder # noqa: E402
def test_ouvert_est_ouvert() -> None:
assert sonder.interpreter("ouvert", None, 0.001)[0] == "ouvert"
def test_refus_immediat_est_une_politique() -> None:
"""Un RST est le signal d'un reject : une POLITIQUE, pas une absence."""
cle, phrase = sonder.interpreter("echec", errno.ECONNREFUSED, 0.002)
assert cle == "refuse"
assert "POLITIQUE" in phrase
def test_le_meme_code_dit_deux_choses_selon_le_delai() -> None:
"""LE TEST QUI COMPTE. `EHOSTUNREACH` lent = machine absente ; rapide = pas de route.
Les confondre est exactement l'erreur du 2026-08-28.
"""
lent, _ = sonder.interpreter("echec", errno.EHOSTUNREACH, 3.05)
vif, _ = sonder.interpreter("echec", errno.EHOSTUNREACH, 0.01)
assert lent == "absent", "un EHOSTUNREACH apres l'ARP dit qu'il n'y a PAS de machine"
assert vif == "injoignable", "le meme code, immediat, dit qu'il n'y a pas de route"
assert lent != vif, "les deux ne doivent JAMAIS se lire pareil"
def test_le_silence_reste_le_silence() -> None:
"""Sans code, on ne conclut pas au-dela de « quelque chose se tait »."""
cle, phrase = sonder.interpreter("echec", None, 6.0)
assert cle == "silence"
assert "frontiere" in phrase
def test_une_passerelle_de_zone_est_signalee_anycast() -> None:
"""La garde qui m'a manque : `10.17.21.1` EST une passerelle de zone."""
assert sonder._est_passerelle("10.17.21.1") == "anycast"
assert sonder._est_passerelle("10.17.19.41") == ""
def test_sans_le_moteur_la_garde_crie_au_lieu_de_se_taire() -> None:
"""Une garde qui echoue doit crier. Ma premiere version rendait « pas anycast »
quand `inventory_rules` manquait — c'est-a-dire sur un hyperviseur ou l'on a copie
le seul fichier, donc precisement la ou le piege se produit."""
vrai_import = __builtins__["__import__"] if isinstance(__builtins__, dict) else __builtins__.__import__
def sans_moteur(nom, *a, **k):
if nom == "inventory_rules":
raise ImportError("simule")
return vrai_import(nom, *a, **k)
cible = __builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__
cible["__import__"] = sans_moteur
try:
assert sonder._est_passerelle("10.17.21.1") == "anycast?", \
"sans le moteur, la garde doit avertir au conditionnel, pas se taire"
assert sonder._est_passerelle("10.17.19.41") == ""
finally:
cible["__import__"] = vrai_import
def main() -> int:
tests = [
test_ouvert_est_ouvert,
test_refus_immediat_est_une_politique,
test_le_meme_code_dit_deux_choses_selon_le_delai,
test_le_silence_reste_le_silence,
test_une_passerelle_de_zone_est_signalee_anycast,
test_sans_le_moteur_la_garde_crie_au_lieu_de_se_taire,
]
definis = {n for n, v in globals().items() if n.startswith("test_") and callable(v)}
oublies = definis - {t.__name__ for t in tests}
assert not oublies, f"test(s) definis mais jamais joues : {sorted(oublies)}"
for t in tests:
t()
print(f"ok {t.__name__}")
print(f"{len(tests)} tests passes.")
return 0
if __name__ == "__main__":
raise SystemExit(main())