test_tray_redemarrer_race.py
Rama main · réplica de lectura del repositorio original.
"""Non-regression d'une course reelle (2026-09-03, signalee par
l'utilisateur : "j'ai fait redemarrer avec le systray, ca s'est arrete,
mais pas reparti") entre le clic sur Redemarrer/Arreter dans l'icone de
zone de notification et la sortie du processus.
redemarrer()/arreter() (tray.py) lancent nettoyer() - et pour redemarrer,
_relancer() ensuite - sur un thread demon separe (necessaire : ce callback
tourne sur le thread de la pompe de messages de l'icone, le geler
empecherait icon.stop() d'etre traite a temps). Mais icon.run() rendait la
main des que icon.stop() etait appele, sans jamais attendre ce thread
demon : blink_cli.py (l'appelant) pouvait alors terminer son propre
nettoyage (redondant mais rapide une fois les workers deja arretes par le
premier) et laisser le processus sortir - tuant net le thread demon avant
qu'il n'ait atteint _relancer() (un thread demon ne survit jamais a la fin
du thread principal). "Redemarrer" arretait alors tout sans jamais rien
relancer, exactement le symptome signale."""
from __future__ import annotations
import os
import tempfile
import time
import unittest
from unittest import mock
os.environ["BLINK_BOOTSTRAP"] = "none"
_TEST_HOME = tempfile.TemporaryDirectory(prefix="blink-tray-race-")
os.environ["BLINK_HOME"] = _TEST_HOME.name
try:
import pystray # noqa: E402
except ImportError:
pystray = None
import tray # noqa: E402
class FauxIcon:
"""Remplace pystray.Icon : aucun vrai backend graphique, run() simule
un clic en appelant directement l'item de menu demande, comme le
ferait un vrai clic utilisateur (item(icon), signature confirmee dans
pystray/_base.py MenuItem.__call__)."""
prochain_libelle_a_cliquer = None
def __init__(self, name, icon_img, title, menu=None):
self.menu = menu
self.arrete = False
def stop(self):
self.arrete = True
def update_menu(self):
pass
def run(self):
items = list(self.menu)
cible = next(i for i in items if FauxIcon.prochain_libelle_a_cliquer in i.text)
cible(self)
@unittest.skipUnless(tray.disponible(), "pystray indisponible dans cet environnement")
class TrayRedemarrerRaceTests(unittest.TestCase):
def _executer_et_cliquer(self, libelle: str, nettoyer):
arret = __import__("threading").Event()
FauxIcon.prochain_libelle_a_cliquer = libelle
with mock.patch.object(pystray, "Icon", FauxIcon):
tray.executer(8765, arret, nettoyer)
def test_redemarrer_attend_relancer_avant_de_rendre_la_main(self) -> None:
ordre = []
def nettoyer_lent():
time.sleep(0.3)
ordre.append("nettoyer")
def faux_relancer(sans_relance):
ordre.append("relancer")
with mock.patch.object(tray, "_relancer", side_effect=faux_relancer):
self._executer_et_cliquer("Redémarrer", nettoyer_lent)
# Le point du correctif : executer() ne doit rendre la main
# qu'apres relancer(), jamais avant - sans quoi blink_cli.py
# pourrait laisser sortir le processus (nettoyage redondant mais
# rapide) et tuer le thread demon avant qu'il n'atteigne
# _relancer(), qui ne s'executerait alors jamais.
self.assertEqual(ordre, ["nettoyer", "relancer"],
"executer() a rendu la main avant la fin de la sequence "
"nettoyer()+_relancer() : la course d'origine est revenue")
def test_arreter_attend_nettoyer_avant_de_rendre_la_main(self) -> None:
ordre = []
def nettoyer_lent():
time.sleep(0.3)
ordre.append("nettoyer")
with mock.patch.object(tray, "_relancer") as faux_relancer:
self._executer_et_cliquer("Arrêter", nettoyer_lent)
self.assertEqual(ordre, ["nettoyer"])
faux_relancer.assert_not_called()
if __name__ == "__main__":
unittest.main()