# -*- coding: utf-8 -*- """ Tests für Network Retry-Mechanismen. """ import asyncio import pytest from datetime import datetime from trixy_core.network.retry import ( NetworkRetry, RetryConfig, RetryStrategy, RetryStats, RetryContext, RetryPolicy, RetryableError, NonRetryableError, with_retry, AGGRESSIVE_RETRY, CONSERVATIVE_RETRY, FAST_FAIL_RETRY, ) class TestRetryConfig: """Tests für RetryConfig.""" def test_default_config(self): """Testet Standard-Konfiguration.""" config = RetryConfig() assert config.max_attempts == 3 assert config.strategy == RetryStrategy.EXPONENTIAL assert config.base_delay == 1.0 assert config.max_delay == 60.0 def test_custom_config(self): """Testet benutzerdefinierte Konfiguration.""" config = RetryConfig( max_attempts=5, strategy=RetryStrategy.LINEAR, base_delay=2.0, jitter=0.2, ) assert config.max_attempts == 5 assert config.strategy == RetryStrategy.LINEAR assert config.base_delay == 2.0 assert config.jitter == 0.2 class TestRetryContext: """Tests für RetryContext.""" def test_context_creation(self): """Testet Context-Erstellung.""" ctx = RetryContext(attempt=1, max_attempts=5) assert ctx.attempt == 1 assert ctx.max_attempts == 5 assert ctx.should_retry is True assert ctx.remaining_attempts == 4 def test_is_last_attempt(self): """Testet letzter Versuch.""" ctx = RetryContext(attempt=5, max_attempts=5) assert ctx.is_last_attempt is True ctx2 = RetryContext(attempt=3, max_attempts=5) assert ctx2.is_last_attempt is False def test_abort(self): """Testet Abbruch.""" ctx = RetryContext(attempt=1, max_attempts=5) ctx.abort() assert ctx.should_retry is False def test_set_next_delay(self): """Testet benutzerdefinierte Verzögerung.""" ctx = RetryContext(attempt=1, max_attempts=5) ctx.set_next_delay(5.0) assert ctx.next_delay == 5.0 class TestNetworkRetry: """Tests für NetworkRetry.""" @pytest.fixture def retry(self): """Erstellt NetworkRetry-Instanz.""" config = RetryConfig( max_attempts=3, base_delay=0.01, # Schnelle Tests jitter=0.0, ) return NetworkRetry(config=config) @pytest.mark.asyncio async def test_successful_operation(self, retry): """Testet erfolgreiche Operation.""" call_count = 0 async def operation(): nonlocal call_count call_count += 1 return "success" result = await retry.execute(operation) assert result == "success" assert call_count == 1 assert retry.stats.successful_attempts == 1 @pytest.mark.asyncio async def test_retry_on_failure(self, retry): """Testet Retry bei Fehler.""" call_count = 0 async def operation(): nonlocal call_count call_count += 1 if call_count < 3: raise ValueError("Fehler") return "success" result = await retry.execute(operation) assert result == "success" assert call_count == 3 assert retry.stats.total_attempts == 3 @pytest.mark.asyncio async def test_max_attempts_exceeded(self, retry): """Testet Überschreitung der maximalen Versuche.""" async def operation(): raise ValueError("Dauerhafter Fehler") with pytest.raises(ValueError): await retry.execute(operation) assert retry.stats.total_attempts == 3 assert retry.stats.failed_attempts == 1 @pytest.mark.asyncio async def test_non_retryable_error(self, retry): """Testet NonRetryableError.""" call_count = 0 async def operation(): nonlocal call_count call_count += 1 raise NonRetryableError("Kein Retry") with pytest.raises(NonRetryableError): await retry.execute(operation) assert call_count == 1 # Kein Retry @pytest.mark.asyncio async def test_retryable_error(self): """Testet RetryableError.""" config = RetryConfig( max_attempts=3, base_delay=0.01, non_retryable_exceptions=(ValueError,), ) retry = NetworkRetry(config=config) call_count = 0 async def operation(): nonlocal call_count call_count += 1 if call_count < 3: raise RetryableError("Retry bitte") return "success" result = await retry.execute(operation) assert result == "success" assert call_count == 3 @pytest.mark.asyncio async def test_exponential_backoff(self): """Testet exponentielles Backoff.""" config = RetryConfig( max_attempts=4, strategy=RetryStrategy.EXPONENTIAL, base_delay=0.01, multiplier=2.0, jitter=0.0, ) retry = NetworkRetry(config=config) delays = [] async def operation(): if retry.stats.total_attempts < 3: raise ValueError("Fehler") return "success" # Backoff: 0.01, 0.02, 0.04 await retry.execute(operation) assert retry.stats.total_attempts == 3 @pytest.mark.asyncio async def test_linear_backoff(self): """Testet lineares Backoff.""" config = RetryConfig( max_attempts=4, strategy=RetryStrategy.LINEAR, base_delay=0.01, jitter=0.0, ) retry = NetworkRetry(config=config) async def operation(): if retry.stats.total_attempts < 3: raise ValueError("Fehler") return "success" # Linear: 0.01, 0.02, 0.03 await retry.execute(operation) assert retry.stats.total_attempts == 3 @pytest.mark.asyncio async def test_constant_backoff(self): """Testet konstantes Backoff.""" config = RetryConfig( max_attempts=3, strategy=RetryStrategy.CONSTANT, base_delay=0.01, jitter=0.0, ) retry = NetworkRetry(config=config) async def operation(): if retry.stats.total_attempts < 2: raise ValueError("Fehler") return "success" await retry.execute(operation) assert retry.stats.total_attempts == 2 @pytest.mark.asyncio async def test_max_delay_limit(self): """Testet maximale Verzögerung.""" config = RetryConfig( max_attempts=5, strategy=RetryStrategy.EXPONENTIAL, base_delay=1.0, max_delay=0.05, multiplier=10.0, jitter=0.0, ) retry = NetworkRetry(config=config) # Verzögerung sollte auf max_delay begrenzt sein ctx = RetryContext(attempt=3, max_attempts=5) delay = retry._calculate_delay(3, ctx) assert delay <= config.max_delay def test_reset_stats(self, retry): """Testet Stats-Reset.""" retry.stats.total_attempts = 10 retry.stats.successful_attempts = 5 retry.reset_stats() assert retry.stats.total_attempts == 0 assert retry.stats.successful_attempts == 0 class TestWithRetryDecorator: """Tests für @with_retry Decorator.""" @pytest.mark.asyncio async def test_decorator_success(self): """Testet erfolgreichen Decorator.""" call_count = 0 @with_retry(max_attempts=3, base_delay=0.01) async def operation(): nonlocal call_count call_count += 1 return "success" result = await operation() assert result == "success" assert call_count == 1 @pytest.mark.asyncio async def test_decorator_retry(self): """Testet Decorator mit Retry.""" call_count = 0 @with_retry(max_attempts=3, base_delay=0.01) async def operation(): nonlocal call_count call_count += 1 if call_count < 3: raise ValueError("Fehler") return "success" result = await operation() assert result == "success" assert call_count == 3 @pytest.mark.asyncio async def test_decorator_failure(self): """Testet Decorator mit Fehlschlag.""" @with_retry(max_attempts=2, base_delay=0.01) async def operation(): raise ValueError("Dauerhafter Fehler") with pytest.raises(ValueError): await operation() class TestRetryPolicy: """Tests für RetryPolicy.""" @pytest.fixture def policy(self): """Erstellt RetryPolicy.""" config = RetryConfig( max_attempts=3, base_delay=0.01, ) return RetryPolicy("test_policy", config) @pytest.mark.asyncio async def test_policy_execute(self, policy): """Testet Policy-Ausführung.""" async def operation(): return "success" result = await policy.execute(operation, "test_op") assert result == "success" @pytest.mark.asyncio async def test_policy_stats(self, policy): """Testet Policy-Statistiken.""" async def operation(): return "success" await policy.execute(operation, "op1") await policy.execute(operation, "op2") stats = policy.get_stats() assert "op1" in stats assert "op2" in stats @pytest.mark.asyncio async def test_policy_single_stats(self, policy): """Testet einzelne Policy-Statistik.""" async def operation(): return "success" await policy.execute(operation, "test_op") stats = policy.get_stats("test_op") assert stats.successful_attempts == 1 class TestPredefinedPolicies: """Tests für vordefinierte Policies.""" def test_aggressive_retry(self): """Testet aggressive Retry-Config.""" assert AGGRESSIVE_RETRY.max_attempts == 10 assert AGGRESSIVE_RETRY.strategy == RetryStrategy.EXPONENTIAL def test_conservative_retry(self): """Testet konservative Retry-Config.""" assert CONSERVATIVE_RETRY.max_attempts == 3 assert CONSERVATIVE_RETRY.strategy == RetryStrategy.LINEAR def test_fast_fail_retry(self): """Testet Fast-Fail Retry-Config.""" assert FAST_FAIL_RETRY.max_attempts == 2 assert FAST_FAIL_RETRY.strategy == RetryStrategy.CONSTANT