OPEN SOURCE
software/tests/test_data.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Tests for data loading and storage."""23import json4import pytest5from datetime import datetime6from pathlib import Path78from src.data.loader import DataLoader9from src.data.storage import Storage10from src.models.verse import (11TranslationOutput,12VerseReference,13)141516@pytest.fixture17def temp_data_dir(tmp_path: Path) -> Path:18"""Create a temporary data directory with test data."""19# Create directory structure20sources_dir = tmp_path / "sources"21(sources_dir / "greek").mkdir(parents=True)22(sources_dir / "japanese" / "meiji").mkdir(parents=True)23(sources_dir / "japanese" / "taisho").mkdir(parents=True)24(sources_dir / "english" / "kjv").mkdir(parents=True)25(sources_dir / "english" / "asv").mkdir(parents=True)26(tmp_path / "metadata" / "john").mkdir(parents=True)27(tmp_path / "output").mkdir(parents=True)28(tmp_path / "progress").mkdir(parents=True)2930# Create test Greek data31greek_data = {32"1:1": {"text": "Ἐν ἀρχῇ ἦν ὁ λόγος", "translit": "En archē ēn ho logos"},33"1:2": {"text": "οὗτος ἦν ἐν ἀρχῇ", "translit": "houtos ēn en archē"},34"1:3": "Verse 3 text only",35}36with (sources_dir / "greek" / "john.json").open("w") as f:37json.dump(greek_data, f)3839# Create test KJV data40kjv_data = {41"1:1": "In the beginning was the Word",42"1:2": "The same was in the beginning with God",43}44with (sources_dir / "english" / "kjv" / "john.json").open("w") as f:45json.dump(kjv_data, f)4647# Create test Meiji data48meiji_data = {49"1:1": "太初に言あり",50}51with (sources_dir / "japanese" / "meiji" / "john.json").open("w") as f:52json.dump(meiji_data, f)5354# Create test metadata55book_metadata = {56"book": "john",57"name_japanese": "ヨハネによる福音書",58"name_english": "Gospel of John",59"testament": "NT",60"author": "John the Apostle",61"themes": ["love", "light"],62"description": "The fourth Gospel",63"chapter_count": 21,64"verse_count": 879,65}66with (tmp_path / "metadata" / "john" / "book_context.json").open("w") as f:67json.dump(book_metadata, f)6869# Create chapter summaries70chapter_summaries = {71"1": {72"summary": "The prologue",73"key_events": ["Word becomes flesh"],74"key_characters": ["Jesus"],75"key_themes": ["Light"],76}77}78with (tmp_path / "metadata" / "john" / "chapter_summaries.json").open("w") as f:79json.dump(chapter_summaries, f)8081# Create glossary82glossary = {83"terms": [84{85"original": "λόγος",86"transliteration": "logos",87"japanese": "言葉",88"english": "word",89"notes": "The Word",90}91]92}93with (tmp_path / "metadata" / "glossary.json").open("w") as f:94json.dump(glossary, f)9596return tmp_path979899class TestDataLoader:100"""Tests for DataLoader."""101102def test_init(self, temp_data_dir: Path) -> None:103"""Test initializing data loader."""104loader = DataLoader(temp_data_dir)105assert loader.data_dir == temp_data_dir106107def test_load_greek_book(self, temp_data_dir: Path) -> None:108"""Test loading Greek text."""109loader = DataLoader(temp_data_dir)110greek = loader.load_greek_book("john")111assert "1:1" in greek112113def test_load_greek_book_not_found(self, temp_data_dir: Path) -> None:114"""Test loading non-existent Greek text."""115loader = DataLoader(temp_data_dir)116greek = loader.load_greek_book("nonexistent")117assert greek == {}118119def test_load_japanese_meiji(self, temp_data_dir: Path) -> None:120"""Test loading Meiji translation."""121loader = DataLoader(temp_data_dir)122meiji = loader.load_japanese_meiji("john")123assert "1:1" in meiji124125def test_load_japanese_taisho(self, temp_data_dir: Path) -> None:126"""Test loading Taisho translation."""127loader = DataLoader(temp_data_dir)128taisho = loader.load_japanese_taisho("john")129assert taisho == {} # No test data for taisho130131def test_load_english_kjv(self, temp_data_dir: Path) -> None:132"""Test loading KJV translation."""133loader = DataLoader(temp_data_dir)134kjv = loader.load_english_kjv("john")135assert "1:1" in kjv136137def test_load_english_asv(self, temp_data_dir: Path) -> None:138"""Test loading ASV translation."""139loader = DataLoader(temp_data_dir)140asv = loader.load_english_asv("john")141assert asv == {} # No test data for ASV142143def test_load_verse(self, temp_data_dir: Path) -> None:144"""Test loading a verse with all sources."""145loader = DataLoader(temp_data_dir)146ref = VerseReference(book="john", chapter=1, verse=1)147verse = loader.load_verse(ref)148assert verse.greek_text == "Ἐν ἀρχῇ ἦν ὁ λόγος"149assert verse.greek_translit == "En archē ēn ho logos"150assert verse.english_kjv == "In the beginning was the Word"151assert verse.japanese_meiji == "太初に言あり"152153def test_load_verse_with_string_greek(self, temp_data_dir: Path) -> None:154"""Test loading verse with string-only Greek data."""155loader = DataLoader(temp_data_dir)156ref = VerseReference(book="john", chapter=1, verse=3)157verse = loader.load_verse(ref)158assert verse.greek_text == "Verse 3 text only"159assert verse.greek_translit is None160161def test_load_chapter_verses(self, temp_data_dir: Path) -> None:162"""Test loading all verses in a chapter."""163loader = DataLoader(temp_data_dir)164verses = loader.load_chapter_verses("john", 1)165assert len(verses) >= 2 # At least 2 verses in test data166167def test_load_book_metadata(self, temp_data_dir: Path) -> None:168"""Test loading book metadata."""169loader = DataLoader(temp_data_dir)170meta = loader.load_book_metadata("john")171assert meta is not None172assert meta.name_japanese == "ヨハネによる福音書"173174def test_load_book_metadata_not_found(self, temp_data_dir: Path) -> None:175"""Test loading non-existent metadata."""176loader = DataLoader(temp_data_dir)177meta = loader.load_book_metadata("nonexistent")178assert meta is None179180def test_load_chapter_summary(self, temp_data_dir: Path) -> None:181"""Test loading chapter summary."""182loader = DataLoader(temp_data_dir)183summary = loader.load_chapter_summary("john", 1)184assert summary is not None185assert summary.summary == "The prologue"186187def test_load_chapter_summary_not_found(self, temp_data_dir: Path) -> None:188"""Test loading non-existent chapter summary."""189loader = DataLoader(temp_data_dir)190summary = loader.load_chapter_summary("john", 99)191assert summary is None192193def test_load_glossary(self, temp_data_dir: Path) -> None:194"""Test loading glossary."""195loader = DataLoader(temp_data_dir)196glossary = loader.load_glossary()197assert len(glossary) == 1198assert glossary[0].original == "λόγος"199200def test_load_glossary_empty(self, tmp_path: Path) -> None:201"""Test loading empty glossary."""202(tmp_path / "sources").mkdir()203(tmp_path / "metadata").mkdir()204loader = DataLoader(tmp_path)205glossary = loader.load_glossary()206assert glossary == []207208def test_get_book_chapters(self, temp_data_dir: Path) -> None:209"""Test getting chapter list."""210loader = DataLoader(temp_data_dir)211chapters = loader.get_book_chapters("john")212assert 1 in chapters213214def test_get_book_chapters_handles_invalid_keys(self, temp_data_dir: Path) -> None:215"""Test get_book_chapters handles non-numeric keys."""216# Add invalid key to Greek data217greek_file = temp_data_dir / "sources" / "greek" / "john.json"218with greek_file.open("r") as f:219data = json.load(f)220data["invalid"] = "test"221with greek_file.open("w") as f:222json.dump(data, f)223224loader = DataLoader(temp_data_dir)225chapters = loader.get_book_chapters("john")226assert 1 in chapters227228def test_load_hebrew_book(self, temp_data_dir: Path) -> None:229"""Test loading Hebrew text."""230# Create Hebrew source directory and file231hebrew_dir = temp_data_dir / "sources" / "hebrew"232hebrew_dir.mkdir(parents=True, exist_ok=True)233hebrew_data = {234"1:1": {"text": "בְּרֵאשִׁית", "translit": "bereshit"},235}236with (hebrew_dir / "genesis.json").open("w") as f:237json.dump(hebrew_data, f)238239loader = DataLoader(temp_data_dir)240hebrew = loader.load_hebrew_book("genesis")241assert "1:1" in hebrew242243def test_load_verse_with_hebrew(self, temp_data_dir: Path) -> None:244"""Test loading a verse with Hebrew text."""245# Create Hebrew source246hebrew_dir = temp_data_dir / "sources" / "hebrew"247hebrew_dir.mkdir(parents=True, exist_ok=True)248hebrew_data = {249"1:1": {"text": "בְּרֵאשִׁית", "translit": "bereshit"},250}251with (hebrew_dir / "genesis.json").open("w") as f:252json.dump(hebrew_data, f)253254loader = DataLoader(temp_data_dir)255ref = VerseReference(book="genesis", chapter=1, verse=1)256verse = loader.load_verse(ref)257assert verse.hebrew_text == "בְּרֵאשִׁית"258assert verse.hebrew_translit == "bereshit"259260def test_load_verse_with_hebrew_string_only(self, temp_data_dir: Path) -> None:261"""Test loading verse with string-only Hebrew data."""262hebrew_dir = temp_data_dir / "sources" / "hebrew"263hebrew_dir.mkdir(parents=True, exist_ok=True)264hebrew_data = {265"1:1": "בְּרֵאשִׁית", # String instead of dict266}267with (hebrew_dir / "genesis.json").open("w") as f:268json.dump(hebrew_data, f)269270loader = DataLoader(temp_data_dir)271ref = VerseReference(book="genesis", chapter=1, verse=1)272verse = loader.load_verse(ref)273assert verse.hebrew_text == "בְּרֵאשִׁית"274assert verse.hebrew_translit is None275276277class TestStorage:278"""Tests for Storage."""279280def test_init(self, temp_data_dir: Path) -> None:281"""Test initializing storage."""282storage = Storage(temp_data_dir)283assert storage.data_dir == temp_data_dir284285def test_save_and_load_translation(self, temp_data_dir: Path) -> None:286"""Test saving and loading a translation."""287storage = Storage(temp_data_dir)288ref = VerseReference(book="john", chapter=1, verse=1)289output = TranslationOutput(290reference=ref,291text="初めに言葉があった。",292model="gpt-4o",293sources_used=["greek", "kjv"],294glossary_terms_applied=["λόγος"],295)296storage.save_translation(output)297298loaded = storage.load_translation(ref)299assert loaded is not None300assert loaded.text == "初めに言葉があった。"301assert loaded.model == "gpt-4o"302303def test_load_translation_not_found(self, temp_data_dir: Path) -> None:304"""Test loading non-existent translation."""305storage = Storage(temp_data_dir)306ref = VerseReference(book="john", chapter=99, verse=99)307loaded = storage.load_translation(ref)308assert loaded is None309310def test_load_chapter_translations(self, temp_data_dir: Path) -> None:311"""Test loading all translations for a chapter."""312storage = Storage(temp_data_dir)313314# Save a few translations315for verse_num in [1, 2]:316ref = VerseReference(book="john", chapter=1, verse=verse_num)317output = TranslationOutput(318reference=ref,319text=f"Translation {verse_num}",320model="test",321)322storage.save_translation(output)323324translations = storage.load_chapter_translations("john", 1)325assert len(translations) == 2326assert 1 in translations327assert 2 in translations328329def test_is_verse_translated(self, temp_data_dir: Path) -> None:330"""Test checking if verse is translated."""331storage = Storage(temp_data_dir)332ref = VerseReference(book="john", chapter=1, verse=1)333334assert storage.is_verse_translated(ref) is False335336output = TranslationOutput(reference=ref, text="Test", model="test")337storage.save_translation(output)338339assert storage.is_verse_translated(ref) is True340341def test_get_progress(self, temp_data_dir: Path) -> None:342"""Test getting progress."""343storage = Storage(temp_data_dir)344ref = VerseReference(book="john", chapter=1, verse=1)345output = TranslationOutput(reference=ref, text="Test", model="test")346storage.save_translation(output)347348progress = storage.get_progress("john")349assert "completed" in progress350assert "1" in progress["completed"]351352def test_get_completion_stats(self, temp_data_dir: Path) -> None:353"""Test getting completion stats."""354storage = Storage(temp_data_dir)355356# Save some translations357for verse in [1, 2, 3]:358ref = VerseReference(book="john", chapter=1, verse=verse)359output = TranslationOutput(reference=ref, text="Test", model="test")360storage.save_translation(output)361362stats = storage.get_completion_stats("john", 100)363assert stats["completed"] == 3364assert stats["total"] == 100365assert stats["percentage"] == 3.0366367def test_get_completion_stats_zero_total(self, temp_data_dir: Path) -> None:368"""Test completion stats with zero total."""369storage = Storage(temp_data_dir)370stats = storage.get_completion_stats("john", 0)371assert stats["percentage"] == 0372373def test_save_and_load_translation_with_furigana(374self, temp_data_dir: Path375) -> None:376"""Test saving and loading a translation with furigana."""377storage = Storage(temp_data_dir)378ref = VerseReference(book="john", chapter=1, verse=1)379annotations = [380{381"start_offset": 0,382"end_offset": 1,383"base_text": "神",384"reading": "かみ",385},386{387"start_offset": 2,388"end_offset": 3,389"base_text": "愛",390"reading": "あい",391},392]393output = TranslationOutput(394reference=ref,395text="神は愛です",396text_with_furigana="神[かみ]は愛[あい]です",397furigana_annotations=annotations,398model="gpt-4o",399)400storage.save_translation(output)401402loaded = storage.load_translation(ref)403assert loaded is not None404assert loaded.text == "神は愛です"405assert loaded.text_with_furigana == "神[かみ]は愛[あい]です"406assert len(loaded.furigana_annotations) == 2407assert loaded.furigana_annotations[0]["base_text"] == "神"408assert loaded.furigana_annotations[1]["reading"] == "あい"409410def test_load_translation_backward_compatibility(411self, temp_data_dir: Path412) -> None:413"""Test loading translation without furigana fields (backward compatibility)."""414storage = Storage(temp_data_dir)415ref = VerseReference(book="john", chapter=1, verse=1)416417# Manually create a chapter file without furigana fields418chapter_file = storage.output_dir / "john" / "chapter_01.json"419chapter_file.parent.mkdir(parents=True, exist_ok=True)420chapter_data = {421"book": "john",422"chapter": 1,423"verses": {424"1": {425"text": "初めに言葉があった。",426"model": "test-model",427"timestamp": "2024-01-01T00:00:00",428"prompt_version": "v1.0",429"sources_used": [],430"glossary_terms_applied": [],431}432},433}434with chapter_file.open("w", encoding="utf-8") as f:435json.dump(chapter_data, f)436437loaded = storage.load_translation(ref)438assert loaded is not None439assert loaded.text == "初めに言葉があった。"440assert loaded.text_with_furigana == ""441assert loaded.furigana_annotations == []442443def test_load_chapter_translations_with_furigana(444self, temp_data_dir: Path445) -> None:446"""Test loading chapter translations includes furigana fields."""447storage = Storage(temp_data_dir)448449# Save translations with furigana450for verse_num in [1, 2]:451ref = VerseReference(book="john", chapter=1, verse=verse_num)452output = TranslationOutput(453reference=ref,454text=f"翻訳{verse_num}",455text_with_furigana=f"翻訳[ほんやく]{verse_num}",456furigana_annotations=[457{458"start_offset": 0,459"end_offset": 2,460"base_text": "翻訳",461"reading": "ほんやく",462}463],464model="test",465)466storage.save_translation(output)467468translations = storage.load_chapter_translations("john", 1)469assert len(translations) == 2470assert translations[1].text_with_furigana == "翻訳[ほんやく]1"471assert len(translations[1].furigana_annotations) == 1472473474class TestStorageArchive:475"""Tests for archive storage functionality."""476477def test_init_with_archive_disabled(self, temp_data_dir: Path) -> None:478"""Test storage init with archive disabled by default."""479storage = Storage(temp_data_dir)480assert storage.enable_archive is False481assert not storage.archive_dir.exists()482483def test_init_with_archive_enabled(self, temp_data_dir: Path) -> None:484"""Test storage init with archive enabled."""485storage = Storage(temp_data_dir, enable_archive=True)486assert storage.enable_archive is True487assert storage.archive_dir.exists()488489def test_enable_archive_property(self, temp_data_dir: Path) -> None:490"""Test enable_archive property setter."""491storage = Storage(temp_data_dir)492assert storage.enable_archive is False493494storage.enable_archive = True495assert storage.enable_archive is True496assert storage.archive_dir.exists()497498def test_save_translation_archive(self, temp_data_dir: Path) -> None:499"""Test saving translation archive."""500from src.protos.converters import (501TranslationPromptInputProto,502TranslationResultProto,503VerseProto,504VerseReferenceProto,505)506507storage = Storage(temp_data_dir, enable_archive=True)508ref = VerseReference(book="john", chapter=1, verse=1)509510input_proto = TranslationPromptInputProto(511target_verse=VerseProto(512reference=VerseReferenceProto(book="john", chapter=1, verse=1),513greek_text="Ἐν ἀρχῇ",514),515system_prompt="System",516user_prompt="User",517model="test-model",518)519output_proto = TranslationResultProto(520reference=VerseReferenceProto(book="john", chapter=1, verse=1),521text="初めに",522model="test-model",523)524525archive_path = storage.save_translation_archive(ref, input_proto, output_proto)526527assert archive_path.exists()528assert (archive_path / "verse_01_input.msgpack").exists()529assert (archive_path / "verse_01_output.msgpack").exists()530531def test_load_translation_archive(self, temp_data_dir: Path) -> None:532"""Test loading translation archive."""533from src.protos.converters import (534TranslationPromptInputProto,535TranslationResultProto,536VerseProto,537VerseReferenceProto,538)539540storage = Storage(temp_data_dir, enable_archive=True)541ref = VerseReference(book="john", chapter=1, verse=1)542543input_proto = TranslationPromptInputProto(544target_verse=VerseProto(545reference=VerseReferenceProto(book="john", chapter=1, verse=1),546greek_text="Ἐν ἀρχῇ ἦν ὁ λόγος",547),548system_prompt="You are a translator",549user_prompt="Translate this",550model="claude-3-opus",551)552output_proto = TranslationResultProto(553reference=VerseReferenceProto(book="john", chapter=1, verse=1),554text="初めに言葉があった",555model="claude-3-opus",556input_hash="abc123",557)558559storage.save_translation_archive(ref, input_proto, output_proto)560561result = storage.load_translation_archive(ref)562assert result is not None563loaded_input, loaded_output = result564565assert loaded_input.target_verse.greek_text == "Ἐν ἀρχῇ ἦν ὁ λόγος"566assert loaded_input.system_prompt == "You are a translator"567assert loaded_output.text == "初めに言葉があった"568assert loaded_output.input_hash == "abc123"569570def test_load_translation_archive_not_found(self, temp_data_dir: Path) -> None:571"""Test loading non-existent archive returns None."""572storage = Storage(temp_data_dir, enable_archive=True)573ref = VerseReference(book="john", chapter=99, verse=99)574575result = storage.load_translation_archive(ref)576assert result is None577578def test_archive_directory_structure(self, temp_data_dir: Path) -> None:579"""Test archive creates proper directory structure."""580from src.protos.converters import (581TranslationPromptInputProto,582TranslationResultProto,583VerseReferenceProto,584)585586storage = Storage(temp_data_dir, enable_archive=True)587588# Save multiple verses589for chapter in [1, 2]:590for verse in [1, 2, 3]:591ref = VerseReference(book="john", chapter=chapter, verse=verse)592input_proto = TranslationPromptInputProto(model="test")593output_proto = TranslationResultProto(594reference=VerseReferenceProto(595book="john", chapter=chapter, verse=verse596),597text=f"Translation {chapter}:{verse}",598model="test",599)600storage.save_translation_archive(ref, input_proto, output_proto)601602# Check directory structure603assert (storage.archive_dir / "john").exists()604assert (storage.archive_dir / "john" / "chapter_01").exists()605assert (storage.archive_dir / "john" / "chapter_02").exists()606607# Check files608ch1_path = storage.archive_dir / "john" / "chapter_01"609assert (ch1_path / "verse_01_input.msgpack").exists()610assert (ch1_path / "verse_02_input.msgpack").exists()611assert (ch1_path / "verse_03_input.msgpack").exists()612613def test_archive_roundtrip_with_provenance(self, temp_data_dir: Path) -> None:614"""Test archive roundtrip includes provenance data."""615from datetime import datetime, timezone616617from src.protos.converters import (618TranslationPromptInputProto,619TranslationProvenanceProto,620TranslationResultProto,621VerseReferenceProto,622)623624storage = Storage(temp_data_dir, enable_archive=True)625ref = VerseReference(book="matthew", chapter=5, verse=3)626627provenance = TranslationProvenanceProto(628source_texts={"greek": "Μακάριοι"},629model="claude-opus",630prompt_version="v2.0",631started_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),632completed_at=datetime(2024, 1, 1, 12, 0, 30, tzinfo=timezone.utc),633)634635input_proto = TranslationPromptInputProto(636system_prompt="Translate",637user_prompt="Verse",638model="claude-opus",639)640output_proto = TranslationResultProto(641reference=VerseReferenceProto(book="matthew", chapter=5, verse=3),642text="心の貧しい人々は幸いです",643model="claude-opus",644provenance=provenance,645)646647storage.save_translation_archive(ref, input_proto, output_proto)648649result = storage.load_translation_archive(ref)650assert result is not None651_, loaded_output = result652653assert loaded_output.provenance is not None654assert loaded_output.provenance.model == "claude-opus"655assert loaded_output.provenance.source_texts == {"greek": "Μακάριοι"}656