# -*- coding: utf-8 -*- """ Tests für Connection Pool Module. Testet Connection, ConnectionPool und HealthChecker. """ import asyncio import time import pytest from trixy_core.network.pool.connection import ( Connection, ConnectionState, ConnectionInfo, ) from trixy_core.network.pool.pool import ( ConnectionPool, PoolConfig, PoolStatistics, PooledConnectionContext, ) from trixy_core.network.pool.health import ( HealthChecker, HealthCheckConfig, HealthCheckResult, HealthStatus, PeriodicHealthChecker, ) # ============================================================================= # ConnectionInfo Tests # ============================================================================= class TestConnectionInfo: """Tests für ConnectionInfo.""" def test_creation(self): """Info erstellen.""" info = ConnectionInfo(host="localhost", port=8080) assert info.host == "localhost" assert info.port == 8080 assert info.use_count == 0 assert info.bytes_sent == 0 def test_age(self): """Alter berechnen.""" info = ConnectionInfo(host="localhost", port=8080) time.sleep(0.1) assert info.age >= 0.1 def test_idle_time(self): """Idle-Zeit berechnen.""" info = ConnectionInfo(host="localhost", port=8080) info.last_used_at = time.monotonic() - 5.0 assert info.idle_time >= 5.0 # ============================================================================= # Connection Tests # ============================================================================= class TestConnection: """Tests für Connection.""" def test_initial_state(self): """Initialer Zustand.""" conn = Connection("localhost", 8080) assert conn.state == ConnectionState.CLOSED assert conn.host == "localhost" assert conn.port == 8080 def test_is_usable_when_closed(self): """Nicht verwendbar wenn geschlossen.""" conn = Connection("localhost", 8080) assert conn.is_usable is False def test_acquire_release(self): """Acquire und Release.""" # Simuliere verbundenen Zustand conn = Connection("localhost", 8080) conn._state = ConnectionState.IDLE conn.acquire() assert conn.state == ConnectionState.IN_USE assert conn.info.use_count == 1 conn.release() assert conn.state == ConnectionState.IDLE def test_to_dict(self): """Zu Dictionary konvertieren.""" conn = Connection("localhost", 8080) data = conn.to_dict() assert data["host"] == "localhost" assert data["port"] == 8080 assert data["state"] == "CLOSED" @pytest.mark.asyncio async def test_close_when_already_closed(self): """Schließen wenn bereits geschlossen.""" conn = Connection("localhost", 8080) await conn.close() # Sollte keine Exception werfen assert conn.state == ConnectionState.CLOSED @pytest.mark.asyncio async def test_context_manager_error_handling(self): """Context-Manager bei Fehler.""" conn = Connection("localhost", 8080) conn._state = ConnectionState.IDLE try: async with conn: assert conn.state == ConnectionState.IN_USE raise ValueError("Test error") except ValueError: pass # Nach Fehler sollte Zustand ERROR sein assert conn.state == ConnectionState.ERROR assert conn.info.errors == 1 # ============================================================================= # PoolConfig Tests # ============================================================================= class TestPoolConfig: """Tests für PoolConfig.""" def test_default_config(self): """Standard-Konfiguration.""" config = PoolConfig() assert config.min_size == 1 assert config.max_size == 10 assert config.max_idle_time == 300.0 assert config.connect_timeout == 10.0 def test_custom_config(self): """Benutzerdefinierte Konfiguration.""" config = PoolConfig( min_size=5, max_size=20, max_idle_time=60.0, max_lifetime=1800.0, ) assert config.min_size == 5 assert config.max_size == 20 assert config.max_idle_time == 60.0 # ============================================================================= # ConnectionPool Tests # ============================================================================= class TestConnectionPool: """Tests für ConnectionPool.""" def test_creation(self): """Pool erstellen.""" pool = ConnectionPool("localhost", 8080) assert pool.host == "localhost" assert pool.port == 8080 assert pool.size == 0 @pytest.mark.asyncio async def test_pool_closed_error(self): """Fehler bei geschlossenem Pool.""" pool = ConnectionPool("localhost", 8080) pool._closed = True with pytest.raises(RuntimeError, match="geschlossen"): await pool.acquire() def test_statistics(self): """Statistiken abrufen.""" pool = ConnectionPool("localhost", 8080) stats = pool.get_statistics() assert isinstance(stats, PoolStatistics) assert stats.total_connections == 0 assert stats.idle_connections == 0 def test_to_dict(self): """Zu Dictionary konvertieren.""" config = PoolConfig(min_size=2, max_size=5) pool = ConnectionPool("localhost", 8080, config) data = pool.to_dict() assert data["host"] == "localhost" assert data["port"] == 8080 assert data["config"]["min_size"] == 2 assert data["config"]["max_size"] == 5 # ============================================================================= # HealthCheckResult Tests # ============================================================================= class TestHealthCheckResult: """Tests für HealthCheckResult.""" def test_creation(self): """Result erstellen.""" result = HealthCheckResult( status=HealthStatus.HEALTHY, latency=0.05, message="" ) assert result.status == HealthStatus.HEALTHY assert result.latency == 0.05 assert result.timestamp > 0 def test_unhealthy_with_message(self): """Unhealthy mit Nachricht.""" result = HealthCheckResult( status=HealthStatus.UNHEALTHY, latency=1.0, message="Connection timeout" ) assert result.status == HealthStatus.UNHEALTHY assert result.message == "Connection timeout" # ============================================================================= # HealthCheckConfig Tests # ============================================================================= class TestHealthCheckConfig: """Tests für HealthCheckConfig.""" def test_default_config(self): """Standard-Konfiguration.""" config = HealthCheckConfig() assert config.interval == 30.0 assert config.timeout == 5.0 assert config.healthy_threshold == 2 assert config.unhealthy_threshold == 3 def test_custom_config(self): """Benutzerdefinierte Konfiguration.""" config = HealthCheckConfig( interval=10.0, timeout=2.0, degraded_latency=0.5, ) assert config.interval == 10.0 assert config.timeout == 2.0 assert config.degraded_latency == 0.5 # ============================================================================= # HealthChecker Tests # ============================================================================= class TestHealthChecker: """Tests für HealthChecker.""" def test_creation(self): """Checker erstellen.""" checker = HealthChecker() assert checker._check_func is None def test_set_check_function(self): """Check-Funktion setzen.""" async def custom_check(conn: Connection) -> HealthCheckResult: return HealthCheckResult(HealthStatus.HEALTHY, 0.01) checker = HealthChecker() checker.set_check_function(custom_check) assert checker._check_func is not None @pytest.mark.asyncio async def test_default_check(self): """Standard-Check.""" checker = HealthChecker() conn = Connection("localhost", 8080) result = await checker.check(conn) # Ohne echte Verbindung sollte UNHEALTHY sein assert result.status == HealthStatus.UNHEALTHY @pytest.mark.asyncio async def test_custom_check_function(self): """Benutzerdefinierte Check-Funktion.""" async def always_healthy(conn: Connection) -> HealthCheckResult: return HealthCheckResult(HealthStatus.HEALTHY, 0.001) checker = HealthChecker() checker.set_check_function(always_healthy) conn = Connection("localhost", 8080) result = await checker.check(conn) assert result.status == HealthStatus.HEALTHY @pytest.mark.asyncio async def test_check_timeout(self): """Check mit Timeout.""" async def slow_check(conn: Connection) -> HealthCheckResult: await asyncio.sleep(10.0) # Länger als Timeout return HealthCheckResult(HealthStatus.HEALTHY, 0.001) config = HealthCheckConfig(timeout=0.1) checker = HealthChecker(config) checker.set_check_function(slow_check) conn = Connection("localhost", 8080) result = await checker.check(conn) assert result.status == HealthStatus.UNHEALTHY assert "Timeout" in result.message def test_get_status_unknown(self): """Status unbekannt ohne Historie.""" checker = HealthChecker() status = checker.get_status("unknown_conn") assert status == HealthStatus.UNKNOWN @pytest.mark.asyncio async def test_status_aggregation(self): """Status-Aggregation über mehrere Checks.""" async def healthy_check(conn: Connection) -> HealthCheckResult: return HealthCheckResult(HealthStatus.HEALTHY, 0.001) config = HealthCheckConfig(healthy_threshold=2) checker = HealthChecker(config) checker.set_check_function(healthy_check) conn = Connection("localhost", 8080) # Mehrere Checks durchführen await checker.check(conn) await checker.check(conn) status = checker.get_status(conn.id) assert status == HealthStatus.HEALTHY def test_get_history(self): """Historie abrufen.""" checker = HealthChecker() # Manuell Historie füllen conn_id = "test_conn" checker._history[conn_id] = [ HealthCheckResult(HealthStatus.HEALTHY, 0.01), HealthCheckResult(HealthStatus.HEALTHY, 0.02), ] history = checker.get_history(conn_id) assert len(history) == 2 def test_get_average_latency(self): """Durchschnittliche Latenz berechnen.""" checker = HealthChecker() conn_id = "test_conn" checker._history[conn_id] = [ HealthCheckResult(HealthStatus.HEALTHY, 0.1), HealthCheckResult(HealthStatus.HEALTHY, 0.2), HealthCheckResult(HealthStatus.HEALTHY, 0.3), ] avg = checker.get_average_latency(conn_id, samples=3) assert abs(avg - 0.2) < 0.001 def test_clear_history(self): """Historie löschen.""" checker = HealthChecker() checker._history["conn1"] = [HealthCheckResult(HealthStatus.HEALTHY, 0.01)] checker._history["conn2"] = [HealthCheckResult(HealthStatus.HEALTHY, 0.01)] checker._status["conn1"] = HealthStatus.HEALTHY checker._status["conn2"] = HealthStatus.HEALTHY # Einzelne Verbindung löschen checker.clear("conn1") assert "conn1" not in checker._history assert "conn2" in checker._history # Alle löschen checker.clear() assert len(checker._history) == 0 def test_statistics(self): """Statistiken abrufen.""" checker = HealthChecker() checker._status["conn1"] = HealthStatus.HEALTHY checker._status["conn2"] = HealthStatus.HEALTHY checker._status["conn3"] = HealthStatus.UNHEALTHY stats = checker.get_statistics() assert stats["total_connections"] == 3 assert stats["healthy"] == 2 assert stats["unhealthy"] == 1 # ============================================================================= # PeriodicHealthChecker Tests # ============================================================================= class TestPeriodicHealthChecker: """Tests für PeriodicHealthChecker.""" def test_add_remove_connection(self): """Verbindung hinzufügen/entfernen.""" checker = HealthChecker() periodic = PeriodicHealthChecker(checker) conn = Connection("localhost", 8080) periodic.add_connection(conn) assert conn in periodic._connections periodic.remove_connection(conn) assert conn not in periodic._connections @pytest.mark.asyncio async def test_start_stop(self): """Start und Stop.""" config = HealthCheckConfig(interval=0.1) checker = HealthChecker(config) periodic = PeriodicHealthChecker(checker) await periodic.start() assert periodic._running is True assert periodic._task is not None await periodic.stop() assert periodic._running is False # ============================================================================= # Integration Tests # ============================================================================= class TestIntegration: """Integrationstests.""" @pytest.mark.asyncio async def test_health_checker_with_unhealthy_callback(self): """Health-Checker mit Callback bei unhealthy.""" unhealthy_connections = [] async def on_unhealthy(conn: Connection): unhealthy_connections.append(conn.id) async def always_unhealthy(conn: Connection) -> HealthCheckResult: return HealthCheckResult(HealthStatus.UNHEALTHY, 0.0, "Always failing") checker = HealthChecker() checker.set_check_function(always_unhealthy) periodic = PeriodicHealthChecker(checker, on_unhealthy=on_unhealthy) conn = Connection("localhost", 8080) periodic.add_connection(conn) # Manuell Check durchführen (statt auf Interval zu warten) result = await checker.check(conn) if result.status == HealthStatus.UNHEALTHY: await on_unhealthy(conn) assert conn.id in unhealthy_connections def test_pool_config_validation(self): """Pool-Konfiguration validieren.""" # min_size > max_size sollte trotzdem funktionieren # (könnte aber logisch problematisch sein) config = PoolConfig(min_size=10, max_size=5) assert config.min_size == 10 assert config.max_size == 5 @pytest.mark.asyncio async def test_connection_lifecycle(self): """Verbindungs-Lebenszyklus.""" conn = Connection("localhost", 8080) # Initial: CLOSED assert conn.state == ConnectionState.CLOSED # Simuliere erfolgreiche Verbindung conn._state = ConnectionState.IDLE # Acquire conn.acquire() assert conn.state == ConnectionState.IN_USE assert conn.info.use_count == 1 # Release conn.release() assert conn.state == ConnectionState.IDLE # Close await conn.close() assert conn.state == ConnectionState.CLOSED # ============================================================================= # Edge Cases # ============================================================================= class TestEdgeCases: """Tests für Randfälle.""" def test_empty_history_average(self): """Durchschnitt bei leerer Historie.""" checker = HealthChecker() avg = checker.get_average_latency("nonexistent") assert avg == 0.0 def test_connection_info_immutable_host(self): """Host ist nach Erstellung fix.""" info = ConnectionInfo(host="original", port=8080) assert info.host == "original" # Host kann nicht geändert werden (Dataclass ohne Setter) @pytest.mark.asyncio async def test_double_start_periodic_checker(self): """Doppelter Start des Periodic Checkers.""" checker = HealthChecker(HealthCheckConfig(interval=10.0)) periodic = PeriodicHealthChecker(checker) await periodic.start() task1 = periodic._task await periodic.start() # Zweiter Start sollte ignoriert werden task2 = periodic._task assert task1 is task2 # Gleicher Task await periodic.stop() @pytest.mark.asyncio async def test_double_stop_periodic_checker(self): """Doppelter Stop des Periodic Checkers.""" checker = HealthChecker() periodic = PeriodicHealthChecker(checker) await periodic.start() await periodic.stop() await periodic.stop() # Zweiter Stop sollte keine Exception werfen assert periodic._running is False def test_health_status_ordering(self): """HealthStatus-Ordnung.""" assert HealthStatus.HEALTHY < HealthStatus.DEGRADED assert HealthStatus.DEGRADED < HealthStatus.UNHEALTHY assert HealthStatus.UNHEALTHY < HealthStatus.UNKNOWN