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