test_tray_fil_principal.py
Rama main · réplica de lectura del repositorio original.
"""Non-regression de l'issue #31 (2026-09-27, macOS 27.2) : « start »
lancait le serveur puis mourait cinq secondes plus tard (SIGTRAP, code
133). Le rapport de plantage de macOS designait un thread secondaire dans
-[NSStatusItem setMenu:] : le thread de rafraichissement du menu
(rafraichir(), tray.py) appelait icon.update_menu(), que pystray transmet
tel quel a AppKit, lequel n'admet les changements d'interface que depuis
le thread principal.
Sous macOS, l'appel passe desormais par PyObjCTools.AppHelper.callAfter,
qui le confie a la boucle principale ; ailleurs, il reste direct. Ces tests
simulent la plateforme et AppHelper, et tournent donc partout : la preuve
sur un vrai Mac reste celle de l'issue."""
from __future__ import annotations
import os
import sys
import tempfile
import threading
import types
import unittest
from unittest import mock
os.environ["BLINK_BOOTSTRAP"] = "none"
_TEST_HOME = tempfile.TemporaryDirectory(prefix="blink-tray-fil-")
os.environ["BLINK_HOME"] = _TEST_HOME.name
try:
import pystray # noqa: E402
except ImportError:
pystray = None
try:
# Menu, rafraîchissement et fil principal vivent dans la bibliothèque
# commune depuis qu'elle est née de tray.py : c'est elle qu'on patche.
from nico579_commons import tray as commun # noqa: E402
except ImportError:
commun = None
import tray # noqa: E402
def _faux_pyobjctools(confies, signal=None):
"""Un paquet PyObjCTools dont AppHelper.callAfter note ce qu'on lui
confie, sans l'executer : il n'y a pas de boucle principale AppKit ici."""
def call_after(fonction, *args, **kwargs):
confies.append(fonction)
if signal is not None:
signal.set()
apphelper = types.ModuleType("PyObjCTools.AppHelper")
apphelper.callAfter = call_after
paquet = types.ModuleType("PyObjCTools")
paquet.AppHelper = apphelper
return {"PyObjCTools": paquet, "PyObjCTools.AppHelper": apphelper}
@unittest.skipIf(commun is None, "nico579_commons indisponible dans cet environnement")
class SurLeFilPrincipalTests(unittest.TestCase):
def test_macos_confie_l_appel_a_la_boucle_principale(self) -> None:
confies, faits = [], []
def fonction():
faits.append(threading.current_thread().name)
with mock.patch.dict(sys.modules, _faux_pyobjctools(confies)):
commun.sur_le_fil_principal(fonction, "darwin")()
self.assertEqual(faits, [], "appel fait directement, hors de la boucle principale")
self.assertEqual(confies, [fonction])
def test_ailleurs_l_appel_reste_direct(self) -> None:
def fonction():
return None
for plateforme in ("win32", "linux"):
with self.subTest(plateforme=plateforme):
self.assertIs(commun.sur_le_fil_principal(fonction, plateforme), fonction)
@unittest.skipUnless(tray.disponible(), "pystray indisponible dans cet environnement")
class RafraichissementDuMenuTests(unittest.TestCase):
"""rafraichir(), le thread d'executer(), face a une icone factice."""
def _executer(self, plateforme: str):
confies, icones = [], []
agi = threading.Event()
class FauxIcon:
def __init__(self, name, icon_img, title, menu=None):
self.directs = []
icones.append(self)
def stop(self):
pass
def update_menu(self):
self.directs.append(threading.current_thread() is threading.main_thread())
agi.set()
def run(self):
agi.wait(timeout=5)
vrai = commun.sur_le_fil_principal
arret = threading.Event()
try:
with mock.patch.object(pystray, "Icon", FauxIcon), \
mock.patch.object(commun, "CADENCE_MENU", 0.01), \
mock.patch.object(commun, "sur_le_fil_principal",
lambda fonction: vrai(fonction, plateforme)), \
mock.patch.dict(sys.modules, _faux_pyobjctools(confies, agi)):
tray.executer(8765, arret, lambda: None)
finally:
arret.set()
self.assertTrue(agi.is_set(), "le thread de rafraichissement n'a jamais agi")
return icones[0], confies
def test_macos_ne_touche_jamais_le_menu_hors_du_thread_principal(self) -> None:
icone, confies = self._executer("darwin")
self.assertEqual(icone.directs, [])
self.assertIn(icone.update_menu, confies)
def test_windows_rafraichit_directement(self) -> None:
icone, confies = self._executer("win32")
self.assertEqual(confies, [])
self.assertTrue(icone.directs)
if __name__ == "__main__":
unittest.main()