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

OPEN SOURCE

software/tests/test_pipeline_extended.py

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

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

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

software/tests/test_pipeline_extended.py463 行このファイルを保存 ↓

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

ファイルの確認情報SHA-256 cc379f768aa8a1b814f186559493e7c49f482a5dc91540205a3f38d67bd21b5b
1"""Extended tests for translation pipeline to improve coverage."""23import json4import pytest5from pathlib import Path6from unittest.mock import AsyncMock, MagicMock, patch78from src.context.builder import ContextBuilder9from src.context.glossary import GlossaryManager10from src.data.loader import DataLoader11from src.data.storage import Storage12from src.exceptions import ClaudeAPIError13from src.models.verse import (14    BookMetadata,15    ChapterSummary,16    GlossaryTerm,17    TranslationContext,18    Verse,19    VerseReference,20)21from src.pipeline.prompt import PromptBuilder22from src.pipeline.translator import Translator232425@pytest.fixture26def temp_data_dir_extended(tmp_path: Path) -> Path:27    """Create temp data directory with more test data."""28    sources_dir = tmp_path / "sources"29    (sources_dir / "greek").mkdir(parents=True)30    (sources_dir / "english" / "kjv").mkdir(parents=True)31    (sources_dir / "english" / "asv").mkdir(parents=True)32    (sources_dir / "japanese" / "meiji").mkdir(parents=True)33    (sources_dir / "japanese" / "taisho").mkdir(parents=True)34    (tmp_path / "metadata" / "john").mkdir(parents=True)35    (tmp_path / "output").mkdir(parents=True)36    (tmp_path / "progress").mkdir(parents=True)3738    # Greek data with multiple verses39    greek_data = {40        "1:1": {"text": "Ἐν ἀρχῇ", "translit": "En archē"},41        "1:2": {"text": "οὗτος ἦν", "translit": "houtos ēn"},42        "1:3": {"text": "πάντα δι' αὐτοῦ", "translit": "panta di autou"},43    }44    with (sources_dir / "greek" / "john.json").open("w") as f:45        json.dump(greek_data, f)4647    kjv_data = {48        "1:1": "In the beginning",49        "1:2": "The same was",50        "1:3": "All things were made",51    }52    with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:53        json.dump(kjv_data, f)5455    # Chapter summary56    chapter_summaries = {57        "1": {58            "summary": "The prologue",59            "key_events": [],60            "key_characters": [],61            "key_themes": [],62        }63    }64    with (tmp_path / "metadata" / "john" / "chapter_summaries.json").open("w") as f:65        json.dump(chapter_summaries, f)6667    # Book metadata68    book_metadata = {69        "book": "john",70        "name_japanese": "ヨハネによる福音書",71        "name_english": "Gospel of John",72        "testament": "NT",73        "author": "John",74        "themes": ["love"],75        "description": "The fourth Gospel",76        "chapter_count": 21,77        "verse_count": 879,78    }79    with (tmp_path / "metadata" / "john" / "book_context.json").open("w") as f:80        json.dump(book_metadata, f)8182    return tmp_path838485class TestPromptBuilderExtended:86    """Extended tests for PromptBuilder."""8788    def test_build_prompt_with_preceding_verses(self) -> None:89        """Test prompt includes preceding verses."""90        glossary = GlossaryManager()91        builder = PromptBuilder(glossary)9293        target_verse = Verse(94            reference=VerseReference(book="john", chapter=1, verse=3),95            greek_text="πάντα δι' αὐτοῦ",96            english_kjv="All things were made by him",97        )98        preceding = [99            Verse(100                reference=VerseReference(book="john", chapter=1, verse=1),101                english_kjv="In the beginning was the Word",102            ),103            Verse(104                reference=VerseReference(book="john", chapter=1, verse=2),105                english_kjv="The same was in the beginning with God",106            ),107        ]108109        context = TranslationContext(110            target_verse=target_verse,111            preceding_verses=preceding,112        )113114        prompt = builder.build_prompt(context)115116        assert "Preceding verses:" in prompt117        assert "In the beginning was the Word" in prompt118119    def test_build_prompt_with_chapter_summary(self) -> None:120        """Test prompt includes chapter summary."""121        glossary = GlossaryManager()122        builder = PromptBuilder(glossary)123124        target_verse = Verse(125            reference=VerseReference(book="john", chapter=1, verse=1),126            greek_text="Ἐν ἀρχῇ",127            english_kjv="In the beginning",128        )129        chapter_summary = ChapterSummary(130            book="john",131            chapter=1,132            summary="This is the prologue to John's Gospel.",133            key_events=[],134            key_characters=[],135            key_themes=[],136        )137138        context = TranslationContext(139            target_verse=target_verse,140            chapter_summary=chapter_summary,141        )142143        prompt = builder.build_prompt(context)144145        assert "Chapter Summary:" in prompt146        assert "This is the prologue" in prompt147148    def test_build_prompt_with_themes(self) -> None:149        """Test prompt includes book themes."""150        glossary = GlossaryManager()151        builder = PromptBuilder(glossary)152153        target_verse = Verse(154            reference=VerseReference(book="john", chapter=1, verse=1),155            greek_text="Ἐν ἀρχῇ",156            english_kjv="In the beginning",157        )158        book_metadata = BookMetadata(159            book="john",160            name_japanese="ヨハネによる福音書",161            name_english="Gospel of John",162            testament="NT",163            author="John",164            themes=["love", "light", "life"],165            description="The fourth Gospel",166            chapter_count=21,167            verse_count=879,168        )169170        context = TranslationContext(171            target_verse=target_verse,172            book_metadata=book_metadata,173        )174175        prompt = builder.build_prompt(context)176177        assert "Themes:" in prompt178        assert "love, light, life" in prompt179180181class TestTranslatorExtended:182    """Extended tests for Translator."""183184    @patch("src.pipeline.translator.subprocess.run")185    def test_translate_chapter(186        self, mock_run: MagicMock, temp_data_dir_extended: Path187    ) -> None:188        """Test translating a full chapter."""189        loader = DataLoader(temp_data_dir_extended)190        storage = Storage(temp_data_dir_extended)191        glossary = GlossaryManager()192        context_builder = ContextBuilder(loader, glossary)193        prompt_builder = PromptBuilder(glossary)194195        translator = Translator(196            context_builder, prompt_builder, storage, rate_limit_delay=0197        )198199        # Mock subprocess.run200        mock_run.return_value = MagicMock(201            returncode=0,202            stdout="翻訳テスト。",203            stderr="",204        )205206        # Track progress207        progress_log = []208209        def progress_callback(current: int, total: int, ref: VerseReference) -> None:210            progress_log.append((current, total, str(ref)))211212        outputs = translator.translate_chapter(213            "john", 1, skip_existing=False, progress_callback=progress_callback214        )215216        assert len(outputs) == 3  # 3 verses in test data217        assert len(progress_log) == 3218219    @patch("src.pipeline.translator.asyncio.create_subprocess_exec")220    @pytest.mark.asyncio221    async def test_translate_verse_async(222        self, mock_subprocess: AsyncMock, temp_data_dir_extended: Path223    ) -> None:224        """Test async translation of a verse."""225        loader = DataLoader(temp_data_dir_extended)226        storage = Storage(temp_data_dir_extended)227        glossary = GlossaryManager()228        context_builder = ContextBuilder(loader, glossary)229        prompt_builder = PromptBuilder(glossary)230231        translator = Translator(context_builder, prompt_builder, storage)232233        # Mock async subprocess234        mock_process = AsyncMock()235        mock_process.returncode = 0236        mock_process.communicate.return_value = ("非同期翻訳テスト。".encode(), b"")237        mock_subprocess.return_value = mock_process238239        ref = VerseReference(book="john", chapter=1, verse=1)240        output = await translator.translate_verse_async(ref, skip_existing=False)241242        assert output.text == "非同期翻訳テスト。"243244    @pytest.mark.asyncio245    async def test_translate_verse_async_skip_existing(246        self, temp_data_dir_extended: Path247    ) -> None:248        """Test async translation skips existing."""249        from src.models.verse import TranslationOutput250251        loader = DataLoader(temp_data_dir_extended)252        storage = Storage(temp_data_dir_extended)253        glossary = GlossaryManager()254        context_builder = ContextBuilder(loader, glossary)255        prompt_builder = PromptBuilder(glossary)256257        translator = Translator(context_builder, prompt_builder, storage)258259        # Save existing260        ref = VerseReference(book="john", chapter=1, verse=1)261        existing = TranslationOutput(262            reference=ref, text="既存の翻訳。", model="test"263        )264        storage.save_translation(existing)265266        output = await translator.translate_verse_async(ref, skip_existing=True)267        assert output.text == "既存の翻訳。"268269    @patch("src.pipeline.translator.asyncio.create_subprocess_exec")270    @pytest.mark.asyncio271    async def test_translate_chapter_async(272        self, mock_subprocess: AsyncMock, temp_data_dir_extended: Path273    ) -> None:274        """Test async translation of a chapter."""275        loader = DataLoader(temp_data_dir_extended)276        storage = Storage(temp_data_dir_extended)277        glossary = GlossaryManager()278        context_builder = ContextBuilder(loader, glossary)279        prompt_builder = PromptBuilder(glossary)280281        translator = Translator(282            context_builder, prompt_builder, storage, rate_limit_delay=0283        )284285        # Mock async subprocess286        mock_process = AsyncMock()287        mock_process.returncode = 0288        mock_process.communicate.return_value = ("非同期チャプター翻訳。".encode(), b"")289        mock_subprocess.return_value = mock_process290291        outputs = await translator.translate_chapter_async(292            "john", 1, skip_existing=False, max_concurrent=2293        )294295        assert len(outputs) == 3296297    @patch("src.pipeline.translator.asyncio.create_subprocess_exec")298    @pytest.mark.asyncio299    async def test_call_claude_async_raises_on_error(300        self, mock_subprocess: AsyncMock, temp_data_dir_extended: Path301    ) -> None:302        """Test _call_claude_async raises ClaudeAPIError on CLI failure."""303        loader = DataLoader(temp_data_dir_extended)304        storage = Storage(temp_data_dir_extended)305        glossary = GlossaryManager()306        context_builder = ContextBuilder(loader, glossary)307        prompt_builder = PromptBuilder(glossary)308309        translator = Translator(context_builder, prompt_builder, storage)310311        # Mock async subprocess with error312        mock_process = AsyncMock()313        mock_process.returncode = 1314        mock_process.communicate.return_value = (b"", b"Error: CLI failed")315        mock_subprocess.return_value = mock_process316317        with pytest.raises(ClaudeAPIError, match="Claude CLI failed"):318            await translator._call_claude_async("system", "user")319320321class TestContextBuilderExtended:322    """Extended tests for ContextBuilder."""323324    def test_find_terms_in_greek_translit_duplicate(self, tmp_path: Path) -> None:325        """Test that duplicate terms are not added from transliteration."""326        sources_dir = tmp_path / "sources"327        (sources_dir / "greek").mkdir(parents=True)328        (sources_dir / "hebrew").mkdir(parents=True)329        (sources_dir / "japanese" / "meiji").mkdir(parents=True)330        (sources_dir / "japanese" / "taisho").mkdir(parents=True)331        (sources_dir / "english" / "kjv").mkdir(parents=True)332        (sources_dir / "english" / "asv").mkdir(parents=True)333        (tmp_path / "metadata").mkdir(parents=True)334335        # Create Greek data with both original and transliteration containing the term336        greek_data = {337            "1:1": {338                "text": "λόγος was here",  # Contains original339                "translit": "logos was here",  # Contains transliteration340            },341        }342        with (sources_dir / "greek" / "john.json").open("w") as f:343            json.dump(greek_data, f)344345        loader = DataLoader(tmp_path)346347        # Create glossary where term can be found in both original and translit348        terms = [349            GlossaryTerm(350                original="λόγος",351                transliteration="logos",352                japanese="言葉",353                english="word",354            )355        ]356        glossary = GlossaryManager(terms)357358        builder = ContextBuilder(loader, glossary)359360        ref = VerseReference(book="john", chapter=1, verse=1)361        context = builder.build_context(ref)362363        # Should only have one term, not duplicates364        assert len(context.glossary_terms) == 1365        assert context.glossary_terms[0].original == "λόγος"366367    def test_find_terms_only_in_transliteration(self, tmp_path: Path) -> None:368        """Test finding terms only in transliteration (not in original)."""369        sources_dir = tmp_path / "sources"370        (sources_dir / "greek").mkdir(parents=True)371        (sources_dir / "hebrew").mkdir(parents=True)372        (sources_dir / "japanese" / "meiji").mkdir(parents=True)373        (sources_dir / "japanese" / "taisho").mkdir(parents=True)374        (sources_dir / "english" / "kjv").mkdir(parents=True)375        (sources_dir / "english" / "asv").mkdir(parents=True)376        (tmp_path / "metadata").mkdir(parents=True)377378        # Create Greek data with only transliteration containing the term379        greek_data = {380            "1:1": {381                "text": "Some other Greek text",  # Does NOT contain original382                "translit": "Some text with logos",  # Contains transliteration383            },384        }385        with (sources_dir / "greek" / "john.json").open("w") as f:386            json.dump(greek_data, f)387388        loader = DataLoader(tmp_path)389390        # Glossary term found only via transliteration391        terms = [392            GlossaryTerm(393                original="λόγος",394                transliteration="logos",395                japanese="言葉",396                english="word",397            )398        ]399        glossary = GlossaryManager(terms)400401        builder = ContextBuilder(loader, glossary)402403        ref = VerseReference(book="john", chapter=1, verse=1)404        context = builder.build_context(ref)405406        # Term should be found via transliteration407        assert len(context.glossary_terms) == 1408        assert context.glossary_terms[0].original == "λόγος"409410    def test_find_terms_in_hebrew(self, tmp_path: Path) -> None:411        """Test finding terms in Hebrew text."""412        # Create test data with Hebrew413        sources_dir = tmp_path / "sources"414        (sources_dir / "greek").mkdir(parents=True)415        (sources_dir / "japanese" / "meiji").mkdir(parents=True)416        (sources_dir / "japanese" / "taisho").mkdir(parents=True)417        (sources_dir / "english" / "kjv").mkdir(parents=True)418        (sources_dir / "english" / "asv").mkdir(parents=True)419        (tmp_path / "metadata").mkdir(parents=True)420421        # Create Hebrew source with proper structure422        (sources_dir / "hebrew").mkdir(parents=True)423        hebrew_data = {424            "1:1": {425                "text": "בְּרֵאשִׁית בָּרָא אֱלֹהִים",426                "translit": "bereshit bara elohim",427            },428        }429        with (sources_dir / "hebrew" / "genesis.json").open("w") as f:430            json.dump(hebrew_data, f)431432        kjv_data = {"1:1": "In the beginning God created"}433        with (sources_dir / "english" / "kjv" / "genesis.json").open("w") as f:434            json.dump(kjv_data, f)435436        loader = DataLoader(tmp_path)437438        # Verify Hebrew is loaded439        verse_ref = VerseReference(book="genesis", chapter=1, verse=1)440        verse = loader.load_verse(verse_ref)441        assert verse.hebrew_text == "בְּרֵאשִׁית בָּרָא אֱלֹהִים"442        assert verse.hebrew_translit == "bereshit bara elohim"443444        # Create glossary with Hebrew term445        terms = [446            GlossaryTerm(447                original="אֱלֹהִים",448                transliteration="elohim",449                japanese="神",450                english="God",451            )452        ]453        glossary = GlossaryManager(terms)454455        builder = ContextBuilder(loader, glossary)456457        ref = VerseReference(book="genesis", chapter=1, verse=1)458        context = builder.build_context(ref)459460        # Should find the Hebrew term461        assert len(context.glossary_terms) > 0462        assert context.glossary_terms[0].original == "אֱלֹהִים"463