公開前の確認用サイト
日常訳NICHIJOYAKUEnglish ↗

OPEN SOURCE

software/tests/test_retry.py

このサイトで読み、必要なファイルを保存できます。

日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。

← オープンソースの案内へ

software/tests/test_retry.py359 行このファイルを保存 ↓

ZIPに含まれるものと同じ公開ソースです。行番号から、その行へのリンクを保存できます。

ファイルの確認情報SHA-256 0524142ebd16acd91e5cf08df63ec9a5cdb761efe3048d0d0cd74d5da8ff87ae
1"""Tests for retry utilities."""23import asyncio4import time5from unittest.mock import MagicMock, patch67import pytest89from src.exceptions import RateLimitError, TranslationError10from src.utils.retry import (11    RetryConfig,12    RetryState,13    async_retry_with_backoff,14    retry_with_backoff,15)161718class TestRetryConfig:19    """Tests for RetryConfig."""2021    def test_default_values(self):22        """Test default configuration values."""23        config = RetryConfig()24        assert config.max_attempts == 325        assert config.base_delay == 1.026        assert config.max_delay == 60.027        assert config.exponential_base == 2.028        assert config.jitter is True2930    def test_custom_values(self):31        """Test custom configuration values."""32        config = RetryConfig(33            max_attempts=5,34            base_delay=2.0,35            max_delay=120.0,36            exponential_base=3.0,37            jitter=False,38        )39        assert config.max_attempts == 540        assert config.base_delay == 2.041        assert config.max_delay == 120.042        assert config.exponential_base == 3.043        assert config.jitter is False4445    def test_get_delay_exponential(self):46        """Test exponential delay calculation."""47        config = RetryConfig(base_delay=1.0, exponential_base=2.0, jitter=False)48        assert config.get_delay(0) == 1.0  # 1 * 2^0 = 149        assert config.get_delay(1) == 2.0  # 1 * 2^1 = 250        assert config.get_delay(2) == 4.0  # 1 * 2^2 = 451        assert config.get_delay(3) == 8.0  # 1 * 2^3 = 85253    def test_get_delay_max_cap(self):54        """Test delay is capped at max_delay."""55        config = RetryConfig(56            base_delay=1.0,57            max_delay=5.0,58            exponential_base=2.0,59            jitter=False,60        )61        assert config.get_delay(0) == 1.062        assert config.get_delay(1) == 2.063        assert config.get_delay(2) == 4.064        assert config.get_delay(3) == 5.0  # Capped at max_delay65        assert config.get_delay(10) == 5.06667    def test_get_delay_with_jitter(self):68        """Test delay includes jitter when enabled."""69        config = RetryConfig(base_delay=10.0, jitter=True)70        delays = [config.get_delay(0) for _ in range(100)]71        # With jitter, delays should vary72        assert len(set(delays)) > 173        # All delays should be within +/- 25% of base74        for delay in delays:75            assert 7.5 <= delay <= 12.57677    def test_get_delay_never_negative(self):78        """Test delay is never negative."""79        config = RetryConfig(base_delay=0.1, jitter=True)80        for _ in range(100):81            assert config.get_delay(0) >= 0828384class TestRetryState:85    """Tests for RetryState."""8687    def test_initial_state(self):88        """Test initial retry state."""89        state = RetryState()90        assert state.attempt == 091        assert state.last_exception is None9293    def test_should_retry_retryable(self):94        """Test should_retry returns True for retryable exceptions."""95        state = RetryState()96        error = RateLimitError("Rate limit")97        assert state.should_retry(error) is True98        assert state.last_exception is error99100    def test_should_retry_non_retryable(self):101        """Test should_retry returns False for non-retryable exceptions."""102        state = RetryState()103        error = ValueError("Not retryable")104        assert state.should_retry(error) is False105106    def test_should_retry_max_attempts_reached(self):107        """Test should_retry returns False when max attempts reached."""108        config = RetryConfig(max_attempts=2)109        state = RetryState(config)110        error = RateLimitError("Rate limit")111112        assert state.should_retry(error) is True113        state.attempt = 1  # After first wait114        assert state.should_retry(error) is False115116    def test_wait_increments_attempt(self):117        """Test wait increments attempt counter."""118        config = RetryConfig(base_delay=0.001, jitter=False)119        state = RetryState(config)120        state.should_retry(RateLimitError("test"))121        assert state.attempt == 0122        state.wait()123        assert state.attempt == 1124125    def test_wait_uses_rate_limit_retry_after(self):126        """Test wait uses retry_after from RateLimitError."""127        config = RetryConfig(base_delay=10.0, jitter=False)128        state = RetryState(config)129        error = RateLimitError("Rate limit", retry_after=0.001)130        state.should_retry(error)131132        start = time.time()133        state.wait()134        elapsed = time.time() - start135136        # Should use retry_after (0.001s) not base_delay (10s)137        assert elapsed < 1.0138139    def test_reset(self):140        """Test reset clears state."""141        state = RetryState()142        state.attempt = 5143        state.last_exception = RateLimitError("test")144145        state.reset()146147        assert state.attempt == 0148        assert state.last_exception is None149150    @pytest.mark.asyncio151    async def test_async_wait(self):152        """Test async_wait works correctly."""153        config = RetryConfig(base_delay=0.001, jitter=False)154        state = RetryState(config)155        state.should_retry(RateLimitError("test"))156157        assert state.attempt == 0158        await state.async_wait()159        assert state.attempt == 1160161162class TestRetryWithBackoff:163    """Tests for retry_with_backoff decorator."""164165    def test_success_no_retry(self):166        """Test successful call doesn't retry."""167        call_count = 0168169        @retry_with_backoff(RetryConfig(max_attempts=3))170        def successful_func():171            nonlocal call_count172            call_count += 1173            return "success"174175        result = successful_func()176        assert result == "success"177        assert call_count == 1178179    def test_retry_on_retryable_error(self):180        """Test retry on retryable error."""181        call_count = 0182        config = RetryConfig(183            max_attempts=3,184            base_delay=0.001,185            jitter=False,186            retryable_exceptions=(RateLimitError,),187        )188189        @retry_with_backoff(config)190        def failing_then_success():191            nonlocal call_count192            call_count += 1193            if call_count < 3:194                raise RateLimitError("Rate limit")195            return "success"196197        result = failing_then_success()198        assert result == "success"199        assert call_count == 3200201    def test_no_retry_on_non_retryable_error(self):202        """Test no retry on non-retryable error."""203        call_count = 0204        config = RetryConfig(205            max_attempts=3,206            retryable_exceptions=(RateLimitError,),207        )208209        @retry_with_backoff(config)210        def failing_func():211            nonlocal call_count212            call_count += 1213            raise ValueError("Not retryable")214215        with pytest.raises(ValueError):216            failing_func()217        assert call_count == 1218219    def test_exhausts_retries(self):220        """Test raises after exhausting retries."""221        call_count = 0222        config = RetryConfig(223            max_attempts=3,224            base_delay=0.001,225            jitter=False,226            retryable_exceptions=(RateLimitError,),227        )228229        @retry_with_backoff(config)230        def always_failing():231            nonlocal call_count232            call_count += 1233            raise RateLimitError("Always fails")234235        with pytest.raises(RateLimitError):236            always_failing()237        assert call_count == 3238239    def test_on_retry_callback(self):240        """Test on_retry callback is called."""241        retry_calls = []242        config = RetryConfig(243            max_attempts=3,244            base_delay=0.001,245            jitter=False,246            retryable_exceptions=(RateLimitError,),247        )248249        def on_retry(exc, attempt):250            retry_calls.append((exc, attempt))251252        @retry_with_backoff(config, on_retry=on_retry)253        def failing_then_success():254            if len(retry_calls) < 2:255                raise RateLimitError("Rate limit")256            return "success"257258        result = failing_then_success()259        assert result == "success"260        assert len(retry_calls) == 2261        assert retry_calls[0][1] == 0262        assert retry_calls[1][1] == 1263264    def test_default_config(self):265        """Test decorator works with default config."""266        call_count = 0267268        @retry_with_backoff()269        def successful_func():270            nonlocal call_count271            call_count += 1272            return "success"273274        result = successful_func()275        assert result == "success"276        assert call_count == 1277278279class TestAsyncRetryWithBackoff:280    """Tests for async_retry_with_backoff decorator."""281282    @pytest.mark.asyncio283    async def test_success_no_retry(self):284        """Test successful async call doesn't retry."""285        call_count = 0286287        @async_retry_with_backoff(RetryConfig(max_attempts=3))288        async def successful_func():289            nonlocal call_count290            call_count += 1291            return "success"292293        result = await successful_func()294        assert result == "success"295        assert call_count == 1296297    @pytest.mark.asyncio298    async def test_retry_on_retryable_error(self):299        """Test retry on retryable error."""300        call_count = 0301        config = RetryConfig(302            max_attempts=3,303            base_delay=0.001,304            jitter=False,305            retryable_exceptions=(RateLimitError,),306        )307308        @async_retry_with_backoff(config)309        async def failing_then_success():310            nonlocal call_count311            call_count += 1312            if call_count < 3:313                raise RateLimitError("Rate limit")314            return "success"315316        result = await failing_then_success()317        assert result == "success"318        assert call_count == 3319320    @pytest.mark.asyncio321    async def test_no_retry_on_non_retryable_error(self):322        """Test no retry on non-retryable error."""323        call_count = 0324        config = RetryConfig(325            max_attempts=3,326            retryable_exceptions=(RateLimitError,),327        )328329        @async_retry_with_backoff(config)330        async def failing_func():331            nonlocal call_count332            call_count += 1333            raise ValueError("Not retryable")334335        with pytest.raises(ValueError):336            await failing_func()337        assert call_count == 1338339    @pytest.mark.asyncio340    async def test_exhausts_retries(self):341        """Test raises after exhausting retries."""342        call_count = 0343        config = RetryConfig(344            max_attempts=3,345            base_delay=0.001,346            jitter=False,347            retryable_exceptions=(RateLimitError,),348        )349350        @async_retry_with_backoff(config)351        async def always_failing():352            nonlocal call_count353            call_count += 1354            raise RateLimitError("Always fails")355356        with pytest.raises(RateLimitError):357            await always_failing()358        assert call_count == 3359