test_network_pool.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für Connection Pool Module.
  4. Testet Connection, ConnectionPool und HealthChecker.
  5. """
  6. import asyncio
  7. import time
  8. import pytest
  9. from trixy_core.network.pool.connection import (
  10. Connection,
  11. ConnectionState,
  12. ConnectionInfo,
  13. )
  14. from trixy_core.network.pool.pool import (
  15. ConnectionPool,
  16. PoolConfig,
  17. PoolStatistics,
  18. PooledConnectionContext,
  19. )
  20. from trixy_core.network.pool.health import (
  21. HealthChecker,
  22. HealthCheckConfig,
  23. HealthCheckResult,
  24. HealthStatus,
  25. PeriodicHealthChecker,
  26. )
  27. # =============================================================================
  28. # ConnectionInfo Tests
  29. # =============================================================================
  30. class TestConnectionInfo:
  31. """Tests für ConnectionInfo."""
  32. def test_creation(self):
  33. """Info erstellen."""
  34. info = ConnectionInfo(host="localhost", port=8080)
  35. assert info.host == "localhost"
  36. assert info.port == 8080
  37. assert info.use_count == 0
  38. assert info.bytes_sent == 0
  39. def test_age(self):
  40. """Alter berechnen."""
  41. info = ConnectionInfo(host="localhost", port=8080)
  42. time.sleep(0.1)
  43. assert info.age >= 0.1
  44. def test_idle_time(self):
  45. """Idle-Zeit berechnen."""
  46. info = ConnectionInfo(host="localhost", port=8080)
  47. info.last_used_at = time.monotonic() - 5.0
  48. assert info.idle_time >= 5.0
  49. # =============================================================================
  50. # Connection Tests
  51. # =============================================================================
  52. class TestConnection:
  53. """Tests für Connection."""
  54. def test_initial_state(self):
  55. """Initialer Zustand."""
  56. conn = Connection("localhost", 8080)
  57. assert conn.state == ConnectionState.CLOSED
  58. assert conn.host == "localhost"
  59. assert conn.port == 8080
  60. def test_is_usable_when_closed(self):
  61. """Nicht verwendbar wenn geschlossen."""
  62. conn = Connection("localhost", 8080)
  63. assert conn.is_usable is False
  64. def test_acquire_release(self):
  65. """Acquire und Release."""
  66. # Simuliere verbundenen Zustand
  67. conn = Connection("localhost", 8080)
  68. conn._state = ConnectionState.IDLE
  69. conn.acquire()
  70. assert conn.state == ConnectionState.IN_USE
  71. assert conn.info.use_count == 1
  72. conn.release()
  73. assert conn.state == ConnectionState.IDLE
  74. def test_to_dict(self):
  75. """Zu Dictionary konvertieren."""
  76. conn = Connection("localhost", 8080)
  77. data = conn.to_dict()
  78. assert data["host"] == "localhost"
  79. assert data["port"] == 8080
  80. assert data["state"] == "CLOSED"
  81. @pytest.mark.asyncio
  82. async def test_close_when_already_closed(self):
  83. """Schließen wenn bereits geschlossen."""
  84. conn = Connection("localhost", 8080)
  85. await conn.close() # Sollte keine Exception werfen
  86. assert conn.state == ConnectionState.CLOSED
  87. @pytest.mark.asyncio
  88. async def test_context_manager_error_handling(self):
  89. """Context-Manager bei Fehler."""
  90. conn = Connection("localhost", 8080)
  91. conn._state = ConnectionState.IDLE
  92. try:
  93. async with conn:
  94. assert conn.state == ConnectionState.IN_USE
  95. raise ValueError("Test error")
  96. except ValueError:
  97. pass
  98. # Nach Fehler sollte Zustand ERROR sein
  99. assert conn.state == ConnectionState.ERROR
  100. assert conn.info.errors == 1
  101. # =============================================================================
  102. # PoolConfig Tests
  103. # =============================================================================
  104. class TestPoolConfig:
  105. """Tests für PoolConfig."""
  106. def test_default_config(self):
  107. """Standard-Konfiguration."""
  108. config = PoolConfig()
  109. assert config.min_size == 1
  110. assert config.max_size == 10
  111. assert config.max_idle_time == 300.0
  112. assert config.connect_timeout == 10.0
  113. def test_custom_config(self):
  114. """Benutzerdefinierte Konfiguration."""
  115. config = PoolConfig(
  116. min_size=5,
  117. max_size=20,
  118. max_idle_time=60.0,
  119. max_lifetime=1800.0,
  120. )
  121. assert config.min_size == 5
  122. assert config.max_size == 20
  123. assert config.max_idle_time == 60.0
  124. # =============================================================================
  125. # ConnectionPool Tests
  126. # =============================================================================
  127. class TestConnectionPool:
  128. """Tests für ConnectionPool."""
  129. def test_creation(self):
  130. """Pool erstellen."""
  131. pool = ConnectionPool("localhost", 8080)
  132. assert pool.host == "localhost"
  133. assert pool.port == 8080
  134. assert pool.size == 0
  135. @pytest.mark.asyncio
  136. async def test_pool_closed_error(self):
  137. """Fehler bei geschlossenem Pool."""
  138. pool = ConnectionPool("localhost", 8080)
  139. pool._closed = True
  140. with pytest.raises(RuntimeError, match="geschlossen"):
  141. await pool.acquire()
  142. def test_statistics(self):
  143. """Statistiken abrufen."""
  144. pool = ConnectionPool("localhost", 8080)
  145. stats = pool.get_statistics()
  146. assert isinstance(stats, PoolStatistics)
  147. assert stats.total_connections == 0
  148. assert stats.idle_connections == 0
  149. def test_to_dict(self):
  150. """Zu Dictionary konvertieren."""
  151. config = PoolConfig(min_size=2, max_size=5)
  152. pool = ConnectionPool("localhost", 8080, config)
  153. data = pool.to_dict()
  154. assert data["host"] == "localhost"
  155. assert data["port"] == 8080
  156. assert data["config"]["min_size"] == 2
  157. assert data["config"]["max_size"] == 5
  158. # =============================================================================
  159. # HealthCheckResult Tests
  160. # =============================================================================
  161. class TestHealthCheckResult:
  162. """Tests für HealthCheckResult."""
  163. def test_creation(self):
  164. """Result erstellen."""
  165. result = HealthCheckResult(
  166. status=HealthStatus.HEALTHY,
  167. latency=0.05,
  168. message=""
  169. )
  170. assert result.status == HealthStatus.HEALTHY
  171. assert result.latency == 0.05
  172. assert result.timestamp > 0
  173. def test_unhealthy_with_message(self):
  174. """Unhealthy mit Nachricht."""
  175. result = HealthCheckResult(
  176. status=HealthStatus.UNHEALTHY,
  177. latency=1.0,
  178. message="Connection timeout"
  179. )
  180. assert result.status == HealthStatus.UNHEALTHY
  181. assert result.message == "Connection timeout"
  182. # =============================================================================
  183. # HealthCheckConfig Tests
  184. # =============================================================================
  185. class TestHealthCheckConfig:
  186. """Tests für HealthCheckConfig."""
  187. def test_default_config(self):
  188. """Standard-Konfiguration."""
  189. config = HealthCheckConfig()
  190. assert config.interval == 30.0
  191. assert config.timeout == 5.0
  192. assert config.healthy_threshold == 2
  193. assert config.unhealthy_threshold == 3
  194. def test_custom_config(self):
  195. """Benutzerdefinierte Konfiguration."""
  196. config = HealthCheckConfig(
  197. interval=10.0,
  198. timeout=2.0,
  199. degraded_latency=0.5,
  200. )
  201. assert config.interval == 10.0
  202. assert config.timeout == 2.0
  203. assert config.degraded_latency == 0.5
  204. # =============================================================================
  205. # HealthChecker Tests
  206. # =============================================================================
  207. class TestHealthChecker:
  208. """Tests für HealthChecker."""
  209. def test_creation(self):
  210. """Checker erstellen."""
  211. checker = HealthChecker()
  212. assert checker._check_func is None
  213. def test_set_check_function(self):
  214. """Check-Funktion setzen."""
  215. async def custom_check(conn: Connection) -> HealthCheckResult:
  216. return HealthCheckResult(HealthStatus.HEALTHY, 0.01)
  217. checker = HealthChecker()
  218. checker.set_check_function(custom_check)
  219. assert checker._check_func is not None
  220. @pytest.mark.asyncio
  221. async def test_default_check(self):
  222. """Standard-Check."""
  223. checker = HealthChecker()
  224. conn = Connection("localhost", 8080)
  225. result = await checker.check(conn)
  226. # Ohne echte Verbindung sollte UNHEALTHY sein
  227. assert result.status == HealthStatus.UNHEALTHY
  228. @pytest.mark.asyncio
  229. async def test_custom_check_function(self):
  230. """Benutzerdefinierte Check-Funktion."""
  231. async def always_healthy(conn: Connection) -> HealthCheckResult:
  232. return HealthCheckResult(HealthStatus.HEALTHY, 0.001)
  233. checker = HealthChecker()
  234. checker.set_check_function(always_healthy)
  235. conn = Connection("localhost", 8080)
  236. result = await checker.check(conn)
  237. assert result.status == HealthStatus.HEALTHY
  238. @pytest.mark.asyncio
  239. async def test_check_timeout(self):
  240. """Check mit Timeout."""
  241. async def slow_check(conn: Connection) -> HealthCheckResult:
  242. await asyncio.sleep(10.0) # Länger als Timeout
  243. return HealthCheckResult(HealthStatus.HEALTHY, 0.001)
  244. config = HealthCheckConfig(timeout=0.1)
  245. checker = HealthChecker(config)
  246. checker.set_check_function(slow_check)
  247. conn = Connection("localhost", 8080)
  248. result = await checker.check(conn)
  249. assert result.status == HealthStatus.UNHEALTHY
  250. assert "Timeout" in result.message
  251. def test_get_status_unknown(self):
  252. """Status unbekannt ohne Historie."""
  253. checker = HealthChecker()
  254. status = checker.get_status("unknown_conn")
  255. assert status == HealthStatus.UNKNOWN
  256. @pytest.mark.asyncio
  257. async def test_status_aggregation(self):
  258. """Status-Aggregation über mehrere Checks."""
  259. async def healthy_check(conn: Connection) -> HealthCheckResult:
  260. return HealthCheckResult(HealthStatus.HEALTHY, 0.001)
  261. config = HealthCheckConfig(healthy_threshold=2)
  262. checker = HealthChecker(config)
  263. checker.set_check_function(healthy_check)
  264. conn = Connection("localhost", 8080)
  265. # Mehrere Checks durchführen
  266. await checker.check(conn)
  267. await checker.check(conn)
  268. status = checker.get_status(conn.id)
  269. assert status == HealthStatus.HEALTHY
  270. def test_get_history(self):
  271. """Historie abrufen."""
  272. checker = HealthChecker()
  273. # Manuell Historie füllen
  274. conn_id = "test_conn"
  275. checker._history[conn_id] = [
  276. HealthCheckResult(HealthStatus.HEALTHY, 0.01),
  277. HealthCheckResult(HealthStatus.HEALTHY, 0.02),
  278. ]
  279. history = checker.get_history(conn_id)
  280. assert len(history) == 2
  281. def test_get_average_latency(self):
  282. """Durchschnittliche Latenz berechnen."""
  283. checker = HealthChecker()
  284. conn_id = "test_conn"
  285. checker._history[conn_id] = [
  286. HealthCheckResult(HealthStatus.HEALTHY, 0.1),
  287. HealthCheckResult(HealthStatus.HEALTHY, 0.2),
  288. HealthCheckResult(HealthStatus.HEALTHY, 0.3),
  289. ]
  290. avg = checker.get_average_latency(conn_id, samples=3)
  291. assert abs(avg - 0.2) < 0.001
  292. def test_clear_history(self):
  293. """Historie löschen."""
  294. checker = HealthChecker()
  295. checker._history["conn1"] = [HealthCheckResult(HealthStatus.HEALTHY, 0.01)]
  296. checker._history["conn2"] = [HealthCheckResult(HealthStatus.HEALTHY, 0.01)]
  297. checker._status["conn1"] = HealthStatus.HEALTHY
  298. checker._status["conn2"] = HealthStatus.HEALTHY
  299. # Einzelne Verbindung löschen
  300. checker.clear("conn1")
  301. assert "conn1" not in checker._history
  302. assert "conn2" in checker._history
  303. # Alle löschen
  304. checker.clear()
  305. assert len(checker._history) == 0
  306. def test_statistics(self):
  307. """Statistiken abrufen."""
  308. checker = HealthChecker()
  309. checker._status["conn1"] = HealthStatus.HEALTHY
  310. checker._status["conn2"] = HealthStatus.HEALTHY
  311. checker._status["conn3"] = HealthStatus.UNHEALTHY
  312. stats = checker.get_statistics()
  313. assert stats["total_connections"] == 3
  314. assert stats["healthy"] == 2
  315. assert stats["unhealthy"] == 1
  316. # =============================================================================
  317. # PeriodicHealthChecker Tests
  318. # =============================================================================
  319. class TestPeriodicHealthChecker:
  320. """Tests für PeriodicHealthChecker."""
  321. def test_add_remove_connection(self):
  322. """Verbindung hinzufügen/entfernen."""
  323. checker = HealthChecker()
  324. periodic = PeriodicHealthChecker(checker)
  325. conn = Connection("localhost", 8080)
  326. periodic.add_connection(conn)
  327. assert conn in periodic._connections
  328. periodic.remove_connection(conn)
  329. assert conn not in periodic._connections
  330. @pytest.mark.asyncio
  331. async def test_start_stop(self):
  332. """Start und Stop."""
  333. config = HealthCheckConfig(interval=0.1)
  334. checker = HealthChecker(config)
  335. periodic = PeriodicHealthChecker(checker)
  336. await periodic.start()
  337. assert periodic._running is True
  338. assert periodic._task is not None
  339. await periodic.stop()
  340. assert periodic._running is False
  341. # =============================================================================
  342. # Integration Tests
  343. # =============================================================================
  344. class TestIntegration:
  345. """Integrationstests."""
  346. @pytest.mark.asyncio
  347. async def test_health_checker_with_unhealthy_callback(self):
  348. """Health-Checker mit Callback bei unhealthy."""
  349. unhealthy_connections = []
  350. async def on_unhealthy(conn: Connection):
  351. unhealthy_connections.append(conn.id)
  352. async def always_unhealthy(conn: Connection) -> HealthCheckResult:
  353. return HealthCheckResult(HealthStatus.UNHEALTHY, 0.0, "Always failing")
  354. checker = HealthChecker()
  355. checker.set_check_function(always_unhealthy)
  356. periodic = PeriodicHealthChecker(checker, on_unhealthy=on_unhealthy)
  357. conn = Connection("localhost", 8080)
  358. periodic.add_connection(conn)
  359. # Manuell Check durchführen (statt auf Interval zu warten)
  360. result = await checker.check(conn)
  361. if result.status == HealthStatus.UNHEALTHY:
  362. await on_unhealthy(conn)
  363. assert conn.id in unhealthy_connections
  364. def test_pool_config_validation(self):
  365. """Pool-Konfiguration validieren."""
  366. # min_size > max_size sollte trotzdem funktionieren
  367. # (könnte aber logisch problematisch sein)
  368. config = PoolConfig(min_size=10, max_size=5)
  369. assert config.min_size == 10
  370. assert config.max_size == 5
  371. @pytest.mark.asyncio
  372. async def test_connection_lifecycle(self):
  373. """Verbindungs-Lebenszyklus."""
  374. conn = Connection("localhost", 8080)
  375. # Initial: CLOSED
  376. assert conn.state == ConnectionState.CLOSED
  377. # Simuliere erfolgreiche Verbindung
  378. conn._state = ConnectionState.IDLE
  379. # Acquire
  380. conn.acquire()
  381. assert conn.state == ConnectionState.IN_USE
  382. assert conn.info.use_count == 1
  383. # Release
  384. conn.release()
  385. assert conn.state == ConnectionState.IDLE
  386. # Close
  387. await conn.close()
  388. assert conn.state == ConnectionState.CLOSED
  389. # =============================================================================
  390. # Edge Cases
  391. # =============================================================================
  392. class TestEdgeCases:
  393. """Tests für Randfälle."""
  394. def test_empty_history_average(self):
  395. """Durchschnitt bei leerer Historie."""
  396. checker = HealthChecker()
  397. avg = checker.get_average_latency("nonexistent")
  398. assert avg == 0.0
  399. def test_connection_info_immutable_host(self):
  400. """Host ist nach Erstellung fix."""
  401. info = ConnectionInfo(host="original", port=8080)
  402. assert info.host == "original"
  403. # Host kann nicht geändert werden (Dataclass ohne Setter)
  404. @pytest.mark.asyncio
  405. async def test_double_start_periodic_checker(self):
  406. """Doppelter Start des Periodic Checkers."""
  407. checker = HealthChecker(HealthCheckConfig(interval=10.0))
  408. periodic = PeriodicHealthChecker(checker)
  409. await periodic.start()
  410. task1 = periodic._task
  411. await periodic.start() # Zweiter Start sollte ignoriert werden
  412. task2 = periodic._task
  413. assert task1 is task2 # Gleicher Task
  414. await periodic.stop()
  415. @pytest.mark.asyncio
  416. async def test_double_stop_periodic_checker(self):
  417. """Doppelter Stop des Periodic Checkers."""
  418. checker = HealthChecker()
  419. periodic = PeriodicHealthChecker(checker)
  420. await periodic.start()
  421. await periodic.stop()
  422. await periodic.stop() # Zweiter Stop sollte keine Exception werfen
  423. assert periodic._running is False
  424. def test_health_status_ordering(self):
  425. """HealthStatus-Ordnung."""
  426. assert HealthStatus.HEALTHY < HealthStatus.DEGRADED
  427. assert HealthStatus.DEGRADED < HealthStatus.UNHEALTHY
  428. assert HealthStatus.UNHEALTHY < HealthStatus.UNKNOWN