| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554 |
- # -*- 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
|