OPEN SOURCE
software/tests/test_retry.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Tests for retry utilities."""23import asyncio4import time5from unittest.mock import MagicMock, patch67import pytest89from src.exceptions import RateLimitError, TranslationError10from src.utils.retry import (11RetryConfig,12RetryState,13async_retry_with_backoff,14retry_with_backoff,15)161718class TestRetryConfig:19"""Tests for RetryConfig."""2021def test_default_values(self):22"""Test default configuration values."""23config = RetryConfig()24assert config.max_attempts == 325assert config.base_delay == 1.026assert config.max_delay == 60.027assert config.exponential_base == 2.028assert config.jitter is True2930def test_custom_values(self):31"""Test custom configuration values."""32config = RetryConfig(33max_attempts=5,34base_delay=2.0,35max_delay=120.0,36exponential_base=3.0,37jitter=False,38)39assert config.max_attempts == 540assert config.base_delay == 2.041assert config.max_delay == 120.042assert config.exponential_base == 3.043assert config.jitter is False4445def test_get_delay_exponential(self):46"""Test exponential delay calculation."""47config = RetryConfig(base_delay=1.0, exponential_base=2.0, jitter=False)48assert config.get_delay(0) == 1.0 # 1 * 2^0 = 149assert config.get_delay(1) == 2.0 # 1 * 2^1 = 250assert config.get_delay(2) == 4.0 # 1 * 2^2 = 451assert config.get_delay(3) == 8.0 # 1 * 2^3 = 85253def test_get_delay_max_cap(self):54"""Test delay is capped at max_delay."""55config = RetryConfig(56base_delay=1.0,57max_delay=5.0,58exponential_base=2.0,59jitter=False,60)61assert config.get_delay(0) == 1.062assert config.get_delay(1) == 2.063assert config.get_delay(2) == 4.064assert config.get_delay(3) == 5.0 # Capped at max_delay65assert config.get_delay(10) == 5.06667def test_get_delay_with_jitter(self):68"""Test delay includes jitter when enabled."""69config = RetryConfig(base_delay=10.0, jitter=True)70delays = [config.get_delay(0) for _ in range(100)]71# With jitter, delays should vary72assert len(set(delays)) > 173# All delays should be within +/- 25% of base74for delay in delays:75assert 7.5 <= delay <= 12.57677def test_get_delay_never_negative(self):78"""Test delay is never negative."""79config = RetryConfig(base_delay=0.1, jitter=True)80for _ in range(100):81assert config.get_delay(0) >= 0828384class TestRetryState:85"""Tests for RetryState."""8687def test_initial_state(self):88"""Test initial retry state."""89state = RetryState()90assert state.attempt == 091assert state.last_exception is None9293def test_should_retry_retryable(self):94"""Test should_retry returns True for retryable exceptions."""95state = RetryState()96error = RateLimitError("Rate limit")97assert state.should_retry(error) is True98assert state.last_exception is error99100def test_should_retry_non_retryable(self):101"""Test should_retry returns False for non-retryable exceptions."""102state = RetryState()103error = ValueError("Not retryable")104assert state.should_retry(error) is False105106def test_should_retry_max_attempts_reached(self):107"""Test should_retry returns False when max attempts reached."""108config = RetryConfig(max_attempts=2)109state = RetryState(config)110error = RateLimitError("Rate limit")111112assert state.should_retry(error) is True113state.attempt = 1 # After first wait114assert state.should_retry(error) is False115116def test_wait_increments_attempt(self):117"""Test wait increments attempt counter."""118config = RetryConfig(base_delay=0.001, jitter=False)119state = RetryState(config)120state.should_retry(RateLimitError("test"))121assert state.attempt == 0122state.wait()123assert state.attempt == 1124125def test_wait_uses_rate_limit_retry_after(self):126"""Test wait uses retry_after from RateLimitError."""127config = RetryConfig(base_delay=10.0, jitter=False)128state = RetryState(config)129error = RateLimitError("Rate limit", retry_after=0.001)130state.should_retry(error)131132start = time.time()133state.wait()134elapsed = time.time() - start135136# Should use retry_after (0.001s) not base_delay (10s)137assert elapsed < 1.0138139def test_reset(self):140"""Test reset clears state."""141state = RetryState()142state.attempt = 5143state.last_exception = RateLimitError("test")144145state.reset()146147assert state.attempt == 0148assert state.last_exception is None149150@pytest.mark.asyncio151async def test_async_wait(self):152"""Test async_wait works correctly."""153config = RetryConfig(base_delay=0.001, jitter=False)154state = RetryState(config)155state.should_retry(RateLimitError("test"))156157assert state.attempt == 0158await state.async_wait()159assert state.attempt == 1160161162class TestRetryWithBackoff:163"""Tests for retry_with_backoff decorator."""164165def test_success_no_retry(self):166"""Test successful call doesn't retry."""167call_count = 0168169@retry_with_backoff(RetryConfig(max_attempts=3))170def successful_func():171nonlocal call_count172call_count += 1173return "success"174175result = successful_func()176assert result == "success"177assert call_count == 1178179def test_retry_on_retryable_error(self):180"""Test retry on retryable error."""181call_count = 0182config = RetryConfig(183max_attempts=3,184base_delay=0.001,185jitter=False,186retryable_exceptions=(RateLimitError,),187)188189@retry_with_backoff(config)190def failing_then_success():191nonlocal call_count192call_count += 1193if call_count < 3:194raise RateLimitError("Rate limit")195return "success"196197result = failing_then_success()198assert result == "success"199assert call_count == 3200201def test_no_retry_on_non_retryable_error(self):202"""Test no retry on non-retryable error."""203call_count = 0204config = RetryConfig(205max_attempts=3,206retryable_exceptions=(RateLimitError,),207)208209@retry_with_backoff(config)210def failing_func():211nonlocal call_count212call_count += 1213raise ValueError("Not retryable")214215with pytest.raises(ValueError):216failing_func()217assert call_count == 1218219def test_exhausts_retries(self):220"""Test raises after exhausting retries."""221call_count = 0222config = RetryConfig(223max_attempts=3,224base_delay=0.001,225jitter=False,226retryable_exceptions=(RateLimitError,),227)228229@retry_with_backoff(config)230def always_failing():231nonlocal call_count232call_count += 1233raise RateLimitError("Always fails")234235with pytest.raises(RateLimitError):236always_failing()237assert call_count == 3238239def test_on_retry_callback(self):240"""Test on_retry callback is called."""241retry_calls = []242config = RetryConfig(243max_attempts=3,244base_delay=0.001,245jitter=False,246retryable_exceptions=(RateLimitError,),247)248249def on_retry(exc, attempt):250retry_calls.append((exc, attempt))251252@retry_with_backoff(config, on_retry=on_retry)253def failing_then_success():254if len(retry_calls) < 2:255raise RateLimitError("Rate limit")256return "success"257258result = failing_then_success()259assert result == "success"260assert len(retry_calls) == 2261assert retry_calls[0][1] == 0262assert retry_calls[1][1] == 1263264def test_default_config(self):265"""Test decorator works with default config."""266call_count = 0267268@retry_with_backoff()269def successful_func():270nonlocal call_count271call_count += 1272return "success"273274result = successful_func()275assert result == "success"276assert call_count == 1277278279class TestAsyncRetryWithBackoff:280"""Tests for async_retry_with_backoff decorator."""281282@pytest.mark.asyncio283async def test_success_no_retry(self):284"""Test successful async call doesn't retry."""285call_count = 0286287@async_retry_with_backoff(RetryConfig(max_attempts=3))288async def successful_func():289nonlocal call_count290call_count += 1291return "success"292293result = await successful_func()294assert result == "success"295assert call_count == 1296297@pytest.mark.asyncio298async def test_retry_on_retryable_error(self):299"""Test retry on retryable error."""300call_count = 0301config = RetryConfig(302max_attempts=3,303base_delay=0.001,304jitter=False,305retryable_exceptions=(RateLimitError,),306)307308@async_retry_with_backoff(config)309async def failing_then_success():310nonlocal call_count311call_count += 1312if call_count < 3:313raise RateLimitError("Rate limit")314return "success"315316result = await failing_then_success()317assert result == "success"318assert call_count == 3319320@pytest.mark.asyncio321async def test_no_retry_on_non_retryable_error(self):322"""Test no retry on non-retryable error."""323call_count = 0324config = RetryConfig(325max_attempts=3,326retryable_exceptions=(RateLimitError,),327)328329@async_retry_with_backoff(config)330async def failing_func():331nonlocal call_count332call_count += 1333raise ValueError("Not retryable")334335with pytest.raises(ValueError):336await failing_func()337assert call_count == 1338339@pytest.mark.asyncio340async def test_exhausts_retries(self):341"""Test raises after exhausting retries."""342call_count = 0343config = RetryConfig(344max_attempts=3,345base_delay=0.001,346jitter=False,347retryable_exceptions=(RateLimitError,),348)349350@async_retry_with_backoff(config)351async def always_failing():352nonlocal call_count353call_count += 1354raise RateLimitError("Always fails")355356with pytest.raises(RateLimitError):357await always_failing()358assert call_count == 3359