test_connection_stability.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für Verbindungsstabilität.
  4. Testet, dass Client-Server-Verbindungen über längere Zeit stabil bleiben.
  5. """
  6. import asyncio
  7. import pytest
  8. from datetime import datetime
  9. from unittest.mock import AsyncMock, MagicMock, patch
  10. from trixy_core.network.keepalive import (
  11. KeepAliveManager,
  12. KeepAliveSession,
  13. KeepAliveConfig,
  14. ConnectionState,
  15. )
  16. class TestKeepAliveStability:
  17. """Tests für KeepAlive-Stabilität."""
  18. @pytest.fixture
  19. def keepalive_config(self):
  20. """Schnelle KeepAlive-Konfiguration für Tests."""
  21. return KeepAliveConfig(
  22. ping_interval=1.0, # 1 Sekunde zwischen Pings
  23. pong_timeout=0.5, # 0.5 Sekunden Timeout
  24. max_missed_pongs=3,
  25. initial_delay=0.1, # Kurze initiale Verzögerung
  26. jitter=0.0, # Kein Jitter für deterministische Tests
  27. adaptive=False, # Kein adaptives Intervall
  28. )
  29. @pytest.mark.asyncio
  30. async def test_keepalive_session_basic(self, keepalive_config):
  31. """Test: KeepAlive-Session funktioniert grundlegend."""
  32. ping_count = 0
  33. pong_received = asyncio.Event()
  34. async def mock_ping():
  35. nonlocal ping_count
  36. ping_count += 1
  37. # Simuliere Pong-Empfang nach kurzem Delay
  38. await asyncio.sleep(0.1)
  39. pong_received.set()
  40. session = KeepAliveSession(
  41. connection_id="test-1",
  42. ping_func=mock_ping,
  43. config=keepalive_config,
  44. )
  45. await session.start()
  46. # Warte auf ersten Ping
  47. await asyncio.sleep(0.3)
  48. # Simuliere Pong
  49. session.handle_pong()
  50. # Warte weitere Zeit
  51. await asyncio.sleep(1.5)
  52. # Simuliere weitere Pongs
  53. session.handle_pong()
  54. await asyncio.sleep(0.5)
  55. await session.stop()
  56. assert ping_count >= 1
  57. assert session.stats.missed_pongs < 3
  58. assert session.state != ConnectionState.DISCONNECTED
  59. @pytest.mark.asyncio
  60. async def test_keepalive_survives_without_disconnect(self, keepalive_config):
  61. """Test: Verbindung bleibt bei regelmäßigen Pongs bestehen."""
  62. disconnect_called = False
  63. async def mock_on_disconnect(conn_id):
  64. nonlocal disconnect_called
  65. disconnect_called = True
  66. ping_count = 0
  67. async def mock_ping():
  68. nonlocal ping_count
  69. ping_count += 1
  70. session = KeepAliveSession(
  71. connection_id="test-stable",
  72. ping_func=mock_ping,
  73. config=keepalive_config,
  74. on_disconnect=mock_on_disconnect,
  75. )
  76. await session.start()
  77. # Lasse Session 3 Sekunden laufen und beantworte alle Pings
  78. start_time = asyncio.get_event_loop().time()
  79. while asyncio.get_event_loop().time() - start_time < 3.0:
  80. await asyncio.sleep(0.2)
  81. # Simuliere erfolgreichen Pong
  82. if session._waiting_for_pong:
  83. session.handle_pong()
  84. await session.stop()
  85. # Verbindung sollte NICHT getrennt worden sein
  86. assert not disconnect_called, "Verbindung wurde unerwartet getrennt"
  87. assert session.state != ConnectionState.DISCONNECTED
  88. assert ping_count >= 2, f"Erwartete mindestens 2 Pings, bekam {ping_count}"
  89. @pytest.mark.asyncio
  90. async def test_keepalive_disconnects_on_timeout(self, keepalive_config):
  91. """Test: Verbindung wird bei ausbleibenden Pongs getrennt."""
  92. disconnect_called = False
  93. disconnect_reason = None
  94. async def mock_on_disconnect(conn_id):
  95. nonlocal disconnect_called, disconnect_reason
  96. disconnect_called = True
  97. disconnect_reason = conn_id
  98. async def mock_ping():
  99. # Sende Ping, aber kein Pong kommt zurück
  100. pass
  101. session = KeepAliveSession(
  102. connection_id="test-timeout",
  103. ping_func=mock_ping,
  104. config=keepalive_config,
  105. on_disconnect=mock_on_disconnect,
  106. )
  107. await session.start()
  108. # Warte lange genug für 3 Timeouts
  109. # initial_delay(0.1) + 3 * (ping_interval(1.0) + pong_timeout(0.5)) = 4.6s
  110. await asyncio.sleep(5.0)
  111. await session.stop()
  112. # Verbindung sollte getrennt worden sein
  113. assert disconnect_called, "Disconnect wurde nicht aufgerufen"
  114. assert session.stats.missed_pongs >= 3
  115. @pytest.mark.asyncio
  116. async def test_keepalive_manager_multiple_sessions(self, keepalive_config):
  117. """Test: Manager verwaltet mehrere Sessions korrekt."""
  118. manager = KeepAliveManager(default_config=keepalive_config)
  119. sessions = []
  120. for i in range(3):
  121. async def make_ping(idx=i):
  122. pass
  123. session = await manager.register(
  124. f"session-{i}",
  125. make_ping,
  126. auto_start=True,
  127. )
  128. sessions.append(session)
  129. assert manager.session_count == 3
  130. assert manager.active_count == 3
  131. # Simuliere Pongs für alle Sessions
  132. for i in range(3):
  133. manager.handle_pong(f"session-{i}")
  134. await asyncio.sleep(0.5)
  135. # Alle Sessions sollten noch aktiv sein
  136. for i in range(3):
  137. stats = manager.get_stats(f"session-{i}")
  138. assert stats is not None
  139. assert stats.state == ConnectionState.CONNECTED
  140. await manager.stop_all()
  141. class TestConnectionStability:
  142. """Integration Tests für Verbindungsstabilität über längere Zeit."""
  143. @pytest.mark.asyncio
  144. @pytest.mark.timeout(80) # 80 Sekunden Timeout für den Test
  145. async def test_connection_stable_for_70_seconds(self):
  146. """
  147. Test: Verbindung bleibt mindestens 70 Sekunden stabil.
  148. Dieser Test simuliert eine Client-Server-Verbindung und
  149. verifiziert, dass bei korrektem Ping/Pong-Austausch die
  150. Verbindung nicht unterbrochen wird.
  151. """
  152. config = KeepAliveConfig(
  153. ping_interval=5.0, # Alle 5 Sekunden
  154. pong_timeout=2.0, # 2 Sekunden Timeout
  155. max_missed_pongs=3,
  156. initial_delay=1.0,
  157. jitter=0.1,
  158. adaptive=False,
  159. )
  160. connection_lost = False
  161. disconnect_reason = ""
  162. async def on_disconnect(conn_id):
  163. nonlocal connection_lost, disconnect_reason
  164. connection_lost = True
  165. disconnect_reason = f"Connection {conn_id} disconnected"
  166. ping_count = 0
  167. pong_count = 0
  168. async def mock_ping():
  169. nonlocal ping_count
  170. ping_count += 1
  171. manager = KeepAliveManager(default_config=config)
  172. manager.on_disconnect(on_disconnect)
  173. session = await manager.register(
  174. "long-test-session",
  175. mock_ping,
  176. auto_start=True,
  177. )
  178. start_time = asyncio.get_event_loop().time()
  179. test_duration = 70.0 # 70 Sekunden
  180. while asyncio.get_event_loop().time() - start_time < test_duration:
  181. await asyncio.sleep(0.5)
  182. # Simuliere Pong-Empfang wenn auf Pong gewartet wird
  183. if session._waiting_for_pong:
  184. session.handle_pong()
  185. pong_count += 1
  186. # Prüfe ob Verbindung noch besteht
  187. if connection_lost:
  188. pytest.fail(
  189. f"Verbindung wurde nach {asyncio.get_event_loop().time() - start_time:.1f}s "
  190. f"unterbrochen. Pings: {ping_count}, Pongs: {pong_count}. "
  191. f"Grund: {disconnect_reason}"
  192. )
  193. await manager.stop_all()
  194. # Finale Assertions
  195. assert not connection_lost, "Verbindung wurde während des 70s-Tests unterbrochen"
  196. assert session.state != ConnectionState.DISCONNECTED
  197. assert ping_count >= 12, f"Zu wenige Pings gesendet: {ping_count}"
  198. assert pong_count >= 12, f"Zu wenige Pongs empfangen: {pong_count}"
  199. assert session.stats.missed_pongs == 0, f"Pongs wurden verpasst: {session.stats.missed_pongs}"
  200. class TestPingPongTimings:
  201. """Tests für Ping/Pong-Timing-Verhalten."""
  202. @pytest.mark.asyncio
  203. async def test_pong_timeout_triggers_correctly(self):
  204. """Test: Pong-Timeout wird korrekt erkannt."""
  205. config = KeepAliveConfig(
  206. ping_interval=0.5,
  207. pong_timeout=0.2,
  208. max_missed_pongs=2,
  209. initial_delay=0.1,
  210. jitter=0.0,
  211. adaptive=False,
  212. )
  213. timeout_count = 0
  214. async def on_timeout(conn_id):
  215. nonlocal timeout_count
  216. timeout_count += 1
  217. async def mock_ping():
  218. pass
  219. session = KeepAliveSession(
  220. connection_id="timeout-test",
  221. ping_func=mock_ping,
  222. config=config,
  223. on_timeout=on_timeout,
  224. )
  225. await session.start()
  226. # Warte auf 2 Timeouts (0.1 initial + 2*(0.5 ping + 0.2 timeout) = 1.5s)
  227. await asyncio.sleep(2.0)
  228. await session.stop()
  229. # Sollte mindestens 2 Timeouts gegeben haben
  230. assert timeout_count >= 2, f"Erwartete mindestens 2 Timeouts, bekam {timeout_count}"
  231. @pytest.mark.asyncio
  232. async def test_late_pong_prevents_timeout(self):
  233. """Test: Später (aber rechtzeitiger) Pong verhindert Timeout."""
  234. config = KeepAliveConfig(
  235. ping_interval=0.3,
  236. pong_timeout=0.5, # Längerer Timeout
  237. max_missed_pongs=3,
  238. initial_delay=0.1,
  239. jitter=0.0,
  240. adaptive=False,
  241. )
  242. timeout_count = 0
  243. async def on_timeout(conn_id):
  244. nonlocal timeout_count
  245. timeout_count += 1
  246. async def mock_ping():
  247. pass
  248. session = KeepAliveSession(
  249. connection_id="late-pong-test",
  250. ping_func=mock_ping,
  251. config=config,
  252. on_timeout=on_timeout,
  253. )
  254. await session.start()
  255. # Reagiere schnell auf Pongs
  256. for _ in range(10):
  257. await asyncio.sleep(0.1)
  258. if session._waiting_for_pong:
  259. # Sende Pong sofort (weit vor dem 0.5s Timeout)
  260. session.handle_pong()
  261. await session.stop()
  262. # Sollte keine Timeouts geben
  263. assert timeout_count == 0, f"Unerwartet {timeout_count} Timeouts"