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

OPEN SOURCE

software/tests/test_final_coverage_2.py

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

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

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

software/tests/test_final_coverage_2.py245 行このファイルを保存 ↓

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

ファイルの確認情報SHA-256 51eaa0d655dd20796234fbb471523c9b6d98d253da9e34b8e5ca1f6b06b22ea6
1"""Final coverage tests - part 2."""23import asyncio4import json5import pytest6from pathlib import Path7from unittest.mock import AsyncMock, MagicMock, patch89from src.context.builder import ContextBuilder10from src.context.glossary import GlossaryManager11from src.context.cross_references import CrossReferenceManager, CrossReference, ReferenceType12from src.context.pericope_generator import PericopeGenerator13from src.data.loader import DataLoader14from src.data.storage import Storage15from src.exceptions import ClaudeAPIError, RateLimitError16from src.models.verse import VerseReference17from src.pipeline.prompt import PromptBuilder18from src.pipeline.translator import Translator19from src.utils.retry import RetryConfig, RetryState202122@pytest.fixture23def temp_data_dir(tmp_path: Path) -> Path:24    """Create temporary data directory."""25    sources_dir = tmp_path / "sources"26    (sources_dir / "greek").mkdir(parents=True)27    (sources_dir / "english" / "kjv").mkdir(parents=True)28    (tmp_path / "metadata").mkdir(parents=True)29    (tmp_path / "output").mkdir(parents=True)3031    greek_data = {32        "1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"},33        "1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"},34    }35    with (sources_dir / "greek" / "john.json").open("w") as f:36        json.dump(greek_data, f)3738    kjv_data = {"1:1": "In the beginning", "1:2": "The same was"}39    with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:40        json.dump(kjv_data, f)4142    return tmp_path434445class TestTranslatorAsyncRetry:46    """Tests for async retry logic in translator."""4748    @patch("src.pipeline.translator.asyncio.create_subprocess_exec")49    @pytest.mark.asyncio50    async def test_call_claude_async_rate_limit_retry(51        self, mock_subprocess: AsyncMock, temp_data_dir: Path52    ) -> None:53        """Test async Claude call retries on rate limit."""54        loader = DataLoader(temp_data_dir)55        storage = Storage(temp_data_dir)56        glossary = GlossaryManager()57        context_builder = ContextBuilder(loader, glossary)58        prompt_builder = PromptBuilder(glossary)5960        retry_config = RetryConfig(61            max_attempts=3,62            base_delay=0.001,63            jitter=False,64            retryable_exceptions=(RateLimitError,),65        )66        translator = Translator(67            context_builder,68            prompt_builder,69            storage,70            retry_config=retry_config,71        )7273        call_count = 07475        async def create_process(*args, **kwargs):76            nonlocal call_count77            call_count += 178            mock_process = AsyncMock()79            if call_count < 3:80                mock_process.returncode = 181                mock_process.communicate.return_value = (b"", b"rate limit exceeded")82            else:83                mock_process.returncode = 084                mock_process.communicate.return_value = ("成功。".encode(), b"")85            return mock_process8687        mock_subprocess.side_effect = create_process8889        result = await translator._call_claude_async("system", "user")90        assert result == "成功。"91        assert call_count == 39293    @patch("src.pipeline.translator.asyncio.create_subprocess_exec")94    @pytest.mark.asyncio95    async def test_call_claude_async_timeout(96        self, mock_subprocess: AsyncMock, temp_data_dir: Path97    ) -> None:98        """Test async Claude call handles timeout."""99        loader = DataLoader(temp_data_dir)100        storage = Storage(temp_data_dir)101        glossary = GlossaryManager()102        context_builder = ContextBuilder(loader, glossary)103        prompt_builder = PromptBuilder(glossary)104105        translator = Translator(106            context_builder,107            prompt_builder,108            storage,109            retry_config=RetryConfig(max_attempts=1, retryable_exceptions=()),110        )111112        async def timeout_process(*args, **kwargs):113            mock_process = AsyncMock()114            mock_process.communicate.side_effect = asyncio.TimeoutError()115            return mock_process116117        mock_subprocess.side_effect = timeout_process118119        with pytest.raises(ClaudeAPIError, match="timed out"):120            await translator._call_claude_async("system", "user")121122123class TestRetryStateEdgeCases:124    """Edge case tests for RetryState."""125126    def test_reset_clears_all_state(self) -> None:127        """Test reset clears attempt count and exception."""128        config = RetryConfig(max_attempts=3, base_delay=0.001)129        state = RetryState(config)130131        # Set some state132        state.attempt = 2133        state.last_exception = RateLimitError("test")134135        state.reset()136137        assert state.attempt == 0138        assert state.last_exception is None139140141class TestCrossReferenceManagerEdgeCases:142    """Additional cross-reference edge cases."""143144    def test_find_references_by_type_no_match(self) -> None:145        """Test finding references by type when none match."""146        manager = CrossReferenceManager()147        manager.add_reference(148            CrossReference(149                source_book="john",150                source_chapter=1,151                source_verse=1,152                target_book="genesis",153                target_chapter=1,154                target_verse_start=1,155                reference_type=ReferenceType.ALLUSION,156            )157        )158159        # Look for QUOTATION but only ALLUSION exists160        found = manager.find_references_by_type("john", 1, 1, ReferenceType.QUOTATION)161        assert len(found) == 0162163    def test_get_parallel_passages_empty(self) -> None:164        """Test getting parallel passages when none exist."""165        manager = CrossReferenceManager()166        manager.add_reference(167            CrossReference(168                source_book="john",169                source_chapter=1,170                source_verse=1,171                target_book="genesis",172                target_chapter=1,173                target_verse_start=1,174                reference_type=ReferenceType.ALLUSION,175            )176        )177178        parallels = manager.get_parallel_passages("john", 1, 1)179        assert len(parallels) == 0180181    def test_get_quotation_sources_empty(self) -> None:182        """Test getting quotation sources when none exist."""183        manager = CrossReferenceManager()184        manager.add_reference(185            CrossReference(186                source_book="john",187                source_chapter=1,188                source_verse=1,189                target_book="genesis",190                target_chapter=1,191                target_verse_start=1,192                reference_type=ReferenceType.THEMATIC,193            )194        )195196        quotes = manager.get_quotation_sources("john", 1, 1)197        assert len(quotes) == 0198199200class TestPericopeGeneratorMerge:201    """Tests for pericope merge functionality."""202203    def test_merge_small_pericopes(self, tmp_path: Path) -> None:204        """Test merging small pericopes into larger ones."""205        # Was a fixed /tmp/test_pericope_merge (via a no-op `with Path(...)`),206        # which concurrent test runs on one machine share and race on.207        greek_dir = tmp_path / "sources" / "greek"208        greek_dir.mkdir(parents=True, exist_ok=True)209210        # Create data with many small paragraph breaks211        greek_data = {212            "1:1": {"text": "¶ verse1"},213            "1:2": {"text": "¶ verse2"},214            "1:3": {"text": "¶ verse3"},215            "1:4": {"text": "verse4"},216            "1:5": {"text": "verse5"},217        }218        with (greek_dir / "test.json").open("w") as f:219            json.dump(greek_data, f)220221        generator = PericopeGenerator(tmp_path)222        # With min_size=3, small pericopes should be merged223        index = generator.generate_pericopes("test", min_size=3)224225        # Should have fewer pericopes due to merging226        assert len(index.pericopes) <= 2227228229class TestValidationPathSafety:230    """Tests for path safety validation."""231232    def test_sanitize_book_already_normalized(self) -> None:233        """Test sanitizing already normalized book name."""234        from src.utils.validation import sanitize_book_name_for_path235236        result = sanitize_book_name_for_path("matthew")237        assert result == "matthew"238239    def test_sanitize_book_with_alias(self) -> None:240        """Test sanitizing book alias."""241        from src.utils.validation import sanitize_book_name_for_path242243        result = sanitize_book_name_for_path("jn")244        assert result == "john"245