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

OPEN SOURCE

software/tests/test_final_coverage.py

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

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

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

software/tests/test_final_coverage.py566 行このファイルを保存 ↓

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

ファイルの確認情報SHA-256 38af1d7758607d7ab97823dc64f68274dc5b7faa2a22ae1c5c2808409f38f3ea
1"""Final coverage tests to reach 98% coverage."""23import asyncio4import json5import pytest6from pathlib import Path7from unittest.mock import AsyncMock, MagicMock, patch89from src.context.builder import ContextBuilder10from src.context.glossary import GlossaryManager11from src.data.loader import DataLoader12from src.data.storage import Storage13from src.exceptions import (14    ChunkParsingError,15    ClaudeAPIError,16    QualityThresholdError,17    RateLimitError,18)19from src.models.evaluation import VerseEvaluation, EvaluationScore20from src.models.glossary import EnhancedGlossaryTerm21from src.models.verse import VerseReference22from src.pipeline.prompt import PromptBuilder23from src.pipeline.translator import Translator24from src.utils.retry import RetryConfig, RetryState, retry_with_backoff, async_retry_with_backoff25from src.utils.validation import parse_reference_string, is_valid_book26from src.exceptions import InvalidReferenceError272829@pytest.fixture30def temp_data_dir(tmp_path: Path) -> Path:31    """Create temporary data directory."""32    sources_dir = tmp_path / "sources"33    (sources_dir / "greek").mkdir(parents=True)34    (sources_dir / "english" / "kjv").mkdir(parents=True)35    (tmp_path / "metadata").mkdir(parents=True)36    (tmp_path / "output").mkdir(parents=True)3738    greek_data = {39        "1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"},40        "1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"},41    }42    with (sources_dir / "greek" / "john.json").open("w") as f:43        json.dump(greek_data, f)4445    kjv_data = {"1:1": "In the beginning", "1:2": "The same was"}46    with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:47        json.dump(kjv_data, f)4849    return tmp_path505152class TestRetryDecoratorEdgeCases:53    """Tests for retry decorator edge cases."""5455    def test_retry_decorator_default_config(self) -> None:56        """Test retry decorator with default config."""57        call_count = 05859        @retry_with_backoff()60        def successful_func():61            nonlocal call_count62            call_count += 163            return "success"6465        result = successful_func()66        assert result == "success"67        assert call_count == 16869    def test_retry_decorator_with_on_retry(self) -> None:70        """Test retry decorator calls on_retry callback."""71        retry_calls = []72        config = RetryConfig(73            max_attempts=3,74            base_delay=0.001,75            jitter=False,76            retryable_exceptions=(RateLimitError,),77        )7879        def on_retry(exc, attempt):80            retry_calls.append((str(exc), attempt))8182        @retry_with_backoff(config, on_retry=on_retry)83        def failing_then_success():84            if len(retry_calls) < 1:85                raise RateLimitError("Rate limit")86            return "success"8788        result = failing_then_success()89        assert result == "success"90        assert len(retry_calls) == 19192    @pytest.mark.asyncio93    async def test_async_retry_decorator_default_config(self) -> None:94        """Test async retry decorator with default config."""95        call_count = 09697        @async_retry_with_backoff()98        async def successful_func():99            nonlocal call_count100            call_count += 1101            return "success"102103        result = await successful_func()104        assert result == "success"105        assert call_count == 1106107    @pytest.mark.asyncio108    async def test_async_retry_with_callback(self) -> None:109        """Test async retry decorator calls on_retry callback."""110        retry_calls = []111        config = RetryConfig(112            max_attempts=3,113            base_delay=0.001,114            jitter=False,115            retryable_exceptions=(RateLimitError,),116        )117118        def on_retry(exc, attempt):119            retry_calls.append((str(exc), attempt))120121        @async_retry_with_backoff(config, on_retry=on_retry)122        async def failing_then_success():123            if len(retry_calls) < 1:124                raise RateLimitError("Rate limit")125            return "success"126127        result = await failing_then_success()128        assert result == "success"129        assert len(retry_calls) == 1130131132class TestRetryStateEdgeCases:133    """Tests for RetryState edge cases."""134135    @pytest.mark.asyncio136    async def test_async_wait_with_rate_limit_retry_after(self) -> None:137        """Test async_wait uses retry_after from rate limit error."""138        config = RetryConfig(139            base_delay=10.0,140            jitter=False,141            retryable_exceptions=(RateLimitError,),142        )143        state = RetryState(config)144145        error = RateLimitError("Rate limit", retry_after=0.001)146        state.should_retry(error)147148        import time149        start = time.time()150        await state.async_wait()151        elapsed = time.time() - start152153        assert elapsed < 1.0154155156class TestValidationEdgeCases:157    """Tests for validation edge cases."""158159    def test_parse_reference_invalid_format_no_colon(self) -> None:160        """Test parsing reference without colon separator."""161        with pytest.raises(InvalidReferenceError, match="Expected format"):162            parse_reference_string("john 1")163164    def test_parse_reference_invalid_chapter(self) -> None:165        """Test parsing reference with non-numeric chapter."""166        with pytest.raises(InvalidReferenceError):167            parse_reference_string("john abc:1")168169    def test_is_valid_book_empty(self) -> None:170        """Test is_valid_book with empty string."""171        assert is_valid_book("") is False172173174class TestGlossaryAddTermEdgeCases:175    """Tests for GlossaryManager add_term edge cases."""176177    def test_add_term_with_all_morphological_fields(self) -> None:178        """Test adding term with all morphological fields."""179        manager = GlossaryManager()180        term = EnhancedGlossaryTerm(181            original="λόγος",182            transliteration="logos",183            japanese="言葉",184            english="word",185            lemma="λογος",186            strongs_number="G3056",187            inflected_forms=["λόγον", "λόγου"],188        )189        manager.add_term(term)190191        # Should be findable by all indexes192        assert manager.get_by_original("λόγος") is not None193        assert manager.get_by_lemma("λογος") is not None194        assert manager.get_by_strongs("G3056") is not None195        assert manager.get_by_any_form("λόγον") is not None196197    def test_add_term_rebuilds_match_pattern(self) -> None:198        """Test that adding term rebuilds the match pattern."""199        manager = GlossaryManager()200        term = EnhancedGlossaryTerm(201            original="λόγος",202            transliteration="logos",203            japanese="言葉",204            english="word",205        )206        manager.add_term(term)207208        # Pattern should now include the term209        found = manager.find_terms_in_text("καὶ ὁ λόγος ἦν")210        assert len(found) == 1211212213class TestTranslatorQualityGateEdgeCases:214    """Tests for translator quality gate edge cases."""215216    @patch("src.pipeline.translator.subprocess.run")217    def test_translate_with_evaluator(218        self, mock_run: MagicMock, temp_data_dir: Path219    ) -> None:220        """Test translation with auto-evaluation."""221        loader = DataLoader(temp_data_dir)222        storage = Storage(temp_data_dir)223        glossary = GlossaryManager()224        context_builder = ContextBuilder(loader, glossary)225        prompt_builder = PromptBuilder(glossary)226227        translator = Translator(228            context_builder,229            prompt_builder,230            storage,231            quality_threshold=70.0,232            enforce_quality_gate=False,233        )234235        mock_run.return_value = MagicMock(236            returncode=0, stdout="翻訳テスト。", stderr=""237        )238239        ref = VerseReference(book="john", chapter=1, verse=1)240        result = translator.translate_verse(ref, skip_existing=False)241242        assert result is not None243        assert result.text == "翻訳テスト。"244245246class TestTranslatorJSONExtractionEdgeCases:247    """Tests for JSON extraction edge cases."""248249    def test_extract_json_from_position_invalid(self, temp_data_dir: Path) -> None:250        """Test extracting JSON from invalid position."""251        loader = DataLoader(temp_data_dir)252        glossary = GlossaryManager()253        storage = Storage(temp_data_dir)254        context_builder = ContextBuilder(loader, glossary)255        prompt_builder = PromptBuilder(glossary)256        translator = Translator(context_builder, prompt_builder, storage)257258        # Text without valid JSON object259        text = "This is just text with no JSON"260        result = translator._extract_json_from_position(text, 0)261        assert result is None262263    def test_extract_json_from_position_nested(self, temp_data_dir: Path) -> None:264        """Test extracting nested JSON object."""265        loader = DataLoader(temp_data_dir)266        glossary = GlossaryManager()267        storage = Storage(temp_data_dir)268        context_builder = ContextBuilder(loader, glossary)269        prompt_builder = PromptBuilder(glossary)270        translator = Translator(context_builder, prompt_builder, storage)271272        # Nested JSON273        text = '{"outer": {"inner": "value"}}'274        result = translator._extract_json_from_position(text, 0)275        assert result is not None276        assert "outer" in result277278    def test_try_parse_json_not_dict(self, temp_data_dir: Path) -> None:279        """Test _try_parse_json returns None for non-dict JSON."""280        loader = DataLoader(temp_data_dir)281        glossary = GlossaryManager()282        storage = Storage(temp_data_dir)283        context_builder = ContextBuilder(loader, glossary)284        prompt_builder = PromptBuilder(glossary)285        translator = Translator(context_builder, prompt_builder, storage)286287        # Valid JSON but not a dict288        result = translator._try_parse_json('["list", "items"]')289        assert result is None290291        # Valid JSON string292        result = translator._try_parse_json('"just a string"')293        assert result is None294295296class TestTranslatorAsyncEdgeCases:297    """Tests for translator async edge cases."""298299    @patch("src.pipeline.translator.asyncio.create_subprocess_exec")300    @pytest.mark.asyncio301    async def test_call_claude_async_success(302        self, mock_subprocess: AsyncMock, temp_data_dir: Path303    ) -> None:304        """Test successful async Claude call."""305        loader = DataLoader(temp_data_dir)306        storage = Storage(temp_data_dir)307        glossary = GlossaryManager()308        context_builder = ContextBuilder(loader, glossary)309        prompt_builder = PromptBuilder(glossary)310311        translator = Translator(312            context_builder,313            prompt_builder,314            storage,315        )316317        mock_process = AsyncMock()318        mock_process.returncode = 0319        mock_process.communicate.return_value = ("非同期翻訳。".encode(), b"")320        mock_subprocess.return_value = mock_process321322        result = await translator._call_claude_async("system", "user")323        assert result == "非同期翻訳。"324325326class TestTranslatorCallClaudeEdgeCases:327    """Tests for _call_claude edge cases."""328329    @patch("src.pipeline.translator.subprocess.run")330    def test_call_claude_with_timeout(331        self, mock_run: MagicMock, temp_data_dir: Path332    ) -> None:333        """Test _call_claude passes timeout correctly."""334        loader = DataLoader(temp_data_dir)335        storage = Storage(temp_data_dir)336        glossary = GlossaryManager()337        context_builder = ContextBuilder(loader, glossary)338        prompt_builder = PromptBuilder(glossary)339340        translator = Translator(341            context_builder,342            prompt_builder,343            storage,344            subprocess_timeout=45,345        )346347        mock_run.return_value = MagicMock(348            returncode=0, stdout="翻訳結果", stderr=""349        )350351        translator._call_claude("system", "user")352353        # Verify timeout was passed354        call_kwargs = mock_run.call_args[1]355        assert call_kwargs["timeout"] == 45356357    @patch("src.pipeline.translator.subprocess.run")358    def test_call_claude_network_error(359        self, mock_run: MagicMock, temp_data_dir: Path360    ) -> None:361        """Test _call_claude handles network errors."""362        loader = DataLoader(temp_data_dir)363        storage = Storage(temp_data_dir)364        glossary = GlossaryManager()365        context_builder = ContextBuilder(loader, glossary)366        prompt_builder = PromptBuilder(glossary)367368        translator = Translator(context_builder, prompt_builder, storage)369370        mock_run.return_value = MagicMock(371            returncode=1, stdout="", stderr="Network connection failed"372        )373374        with pytest.raises(ClaudeAPIError, match="Network error"):375            translator._call_claude("system", "user")376377378class TestGlossaryFindTermsWithPositions:379    """Tests for find_terms_in_text_with_positions."""380381    def test_find_terms_with_positions(self) -> None:382        """Test finding terms with their positions."""383        terms = [384            EnhancedGlossaryTerm(385                original="λόγος",386                transliteration="logos",387                japanese="言葉",388                english="word",389            )390        ]391        manager = GlossaryManager(terms)392393        text = "καὶ ὁ λόγος ἦν"394        found = manager.find_terms_in_text_with_positions(text)395396        assert len(found) == 1397        term, start, end = found[0]398        assert term.original == "λόγος"399        assert start == 6  # Position of λόγος in text400        assert end == 11401402    def test_find_terms_with_positions_empty(self) -> None:403        """Test finding terms with positions when none found."""404        terms = [405            EnhancedGlossaryTerm(406                original="λόγος",407                transliteration="logos",408                japanese="言葉",409                english="word",410            )411        ]412        manager = GlossaryManager(terms)413414        text = "no terms here"415        found = manager.find_terms_in_text_with_positions(text)416417        assert len(found) == 0418419420class TestValidationSanitizePathEdgeCases:421    """Tests for sanitize_book_name_for_path edge cases."""422423    def test_sanitize_path_invalid_characters(self) -> None:424        """Test sanitize path rejects invalid path characters."""425        from src.utils.validation import sanitize_book_name_for_path426427        # Valid book names should pass428        result = sanitize_book_name_for_path("john")429        assert result == "john"430431432class TestTranslatorTimeoutEdgeCases:433    """Tests for translator timeout handling."""434435    @patch("src.pipeline.translator.subprocess.run")436    def test_call_claude_timeout_expired(437        self, mock_run: MagicMock, temp_data_dir: Path438    ) -> None:439        """Test _call_claude handles TimeoutExpired."""440        import subprocess441442        loader = DataLoader(temp_data_dir)443        storage = Storage(temp_data_dir)444        glossary = GlossaryManager()445        context_builder = ContextBuilder(loader, glossary)446        prompt_builder = PromptBuilder(glossary)447448        # Disable retries to test exception directly449        translator = Translator(450            context_builder,451            prompt_builder,452            storage,453            subprocess_timeout=1,454            retry_config=RetryConfig(max_attempts=1, retryable_exceptions=()),455        )456457        # Mock timeout458        mock_run.side_effect = subprocess.TimeoutExpired("claude", 1)459460        with pytest.raises(ClaudeAPIError, match="timed out"):461            translator._call_claude("system", "user")462463    @patch("src.pipeline.translator.subprocess.run")464    def test_call_claude_file_not_found(465        self, mock_run: MagicMock, temp_data_dir: Path466    ) -> None:467        """Test _call_claude handles FileNotFoundError (CLI not installed)."""468        loader = DataLoader(temp_data_dir)469        storage = Storage(temp_data_dir)470        glossary = GlossaryManager()471        context_builder = ContextBuilder(loader, glossary)472        prompt_builder = PromptBuilder(glossary)473474        translator = Translator(context_builder, prompt_builder, storage)475476        # Mock file not found477        mock_run.side_effect = FileNotFoundError("claude not found")478479        from src.exceptions import CLINotFoundError480        with pytest.raises(CLINotFoundError):481            translator._call_claude("system", "user")482483484class TestValidateReferenceExists:485    """Tests for validate_reference_exists function."""486487    def test_validate_reference_exists_not_found(self, temp_data_dir: Path) -> None:488        """Test validate_reference_exists raises when verse not found."""489        from src.utils.validation import validate_reference_exists490        from src.exceptions import VerseNotFoundError491492        loader = DataLoader(temp_data_dir)493494        # Verse 99:99 doesn't exist in our test data495        with pytest.raises(VerseNotFoundError):496            validate_reference_exists(loader, "john", 99, 99)497498    def test_validate_reference_exists_success(self, temp_data_dir: Path) -> None:499        """Test validate_reference_exists passes for existing verse."""500        from src.utils.validation import validate_reference_exists501502        loader = DataLoader(temp_data_dir)503504        # Verse 1:1 exists in our test data505        validate_reference_exists(loader, "john", 1, 1)  # Should not raise506507508class TestCrossReferenceEdgeCases:509    """Additional tests for cross-reference edge cases."""510511    def test_cross_reference_source_ref_property(self) -> None:512        """Test CrossReference source_ref property."""513        from src.context.cross_references import CrossReference514515        ref = CrossReference(516            source_book="john",517            source_chapter=3,518            source_verse=16,519            target_book="genesis",520            target_chapter=1,521            target_verse_start=1,522        )523        assert ref.source_ref == "john 3:16"524525    def test_cross_reference_target_ref_with_range(self) -> None:526        """Test CrossReference target_ref with verse range."""527        from src.context.cross_references import CrossReference528529        ref = CrossReference(530            source_book="john",531            source_chapter=1,532            source_verse=1,533            target_book="genesis",534            target_chapter=1,535            target_verse_start=1,536            target_verse_end=5,537        )538        assert ref.target_ref == "genesis 1:1-5"539540541class TestPericopeGeneratorEdgeCases:542    """Additional tests for pericope generator edge cases."""543544    def test_generate_and_save(self, temp_data_dir: Path) -> None:545        """Test generate_and_save creates output file."""546        from src.context.pericope_generator import PericopeGenerator547548        # Add Greek data549        greek_dir = temp_data_dir / "sources" / "greek"550        greek_data = {551            "1:1": {"text": "¶ Ἐν ἀρχῇ ἦν"},552            "1:2": {"text": "οὗτος ἦν ἐν"},553            "1:3": {"text": "¶ πάντα δι᾽ αὐτοῦ"},554        }555        with (greek_dir / "testbook.json").open("w") as f:556            json.dump(greek_data, f)557558        generator = PericopeGenerator(temp_data_dir)559        index = generator.generate_and_save("testbook")560561        assert index is not None562563        # Check file was created564        output_path = temp_data_dir / "metadata" / "pericopes_generated.json"565        assert output_path.exists()566