OPEN SOURCE
software/tests/test_proto_serialization.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Tests for Protocol Buffer serialization utilities."""23from __future__ import annotations45from dataclasses import dataclass, field6from datetime import datetime, timezone78import pytest910from src.protos.serialization import (11compute_proto_hash,12deserialize_proto,13serialize_proto,14)151617@dataclass18class SimpleProto:19"""Simple test proto."""2021name: str = ""22value: int = 0232425@dataclass26class NestedProto:27"""Proto with nested dataclass."""2829simple: SimpleProto = field(default_factory=SimpleProto)30items: list[str] = field(default_factory=list)313233@dataclass34class DateTimeProto:35"""Proto with datetime fields."""3637created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))38updated_at: datetime | None = None394041@dataclass42class ComplexProto:43"""Proto with various field types."""4445name: str = ""46count: int = 047score: float = 0.048active: bool = False49tags: list[str] = field(default_factory=list)50metadata: dict = field(default_factory=dict)51nested: SimpleProto = field(default_factory=SimpleProto)52nested_list: list[SimpleProto] = field(default_factory=list)53timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))54optional_time: datetime | None = None555657class TestSerializeProto:58"""Tests for serialize_proto function."""5960def test_serialize_simple(self) -> None:61"""Test serializing a simple proto."""62proto = SimpleProto(name="test", value=42)63data = serialize_proto(proto)6465assert isinstance(data, bytes)66assert len(data) > 06768def test_serialize_nested(self) -> None:69"""Test serializing a proto with nested dataclass."""70proto = NestedProto(71simple=SimpleProto(name="nested", value=100),72items=["a", "b", "c"],73)74data = serialize_proto(proto)7576assert isinstance(data, bytes)77assert len(data) > 07879def test_serialize_datetime(self) -> None:80"""Test serializing a proto with datetime fields."""81now = datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)82proto = DateTimeProto(83created_at=now,84updated_at=now,85)86data = serialize_proto(proto)8788assert isinstance(data, bytes)8990def test_serialize_complex(self) -> None:91"""Test serializing a complex proto."""92proto = ComplexProto(93name="complex",94count=100,95score=0.95,96active=True,97tags=["tag1", "tag2"],98metadata={"key": "value"},99nested=SimpleProto(name="inner", value=50),100nested_list=[101SimpleProto(name="item1", value=1),102SimpleProto(name="item2", value=2),103],104timestamp=datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc),105optional_time=None,106)107data = serialize_proto(proto)108109assert isinstance(data, bytes)110111def test_serialize_raises_for_non_dataclass(self) -> None:112"""Test serialize_proto raises TypeError for non-dataclass."""113with pytest.raises(TypeError, match="Expected dataclass instance"):114serialize_proto({"not": "dataclass"})115116def test_serialize_raises_for_class_type(self) -> None:117"""Test serialize_proto raises TypeError for class type."""118with pytest.raises(TypeError, match="Expected dataclass instance"):119serialize_proto(SimpleProto)120121122class TestDeserializeProto:123"""Tests for deserialize_proto function."""124125def test_deserialize_simple(self) -> None:126"""Test deserializing a simple proto."""127original = SimpleProto(name="test", value=42)128data = serialize_proto(original)129result = deserialize_proto(data, SimpleProto)130131assert isinstance(result, SimpleProto)132assert result.name == "test"133assert result.value == 42134135def test_deserialize_nested(self) -> None:136"""Test deserializing a proto with nested dataclass."""137original = NestedProto(138simple=SimpleProto(name="nested", value=100),139items=["a", "b", "c"],140)141data = serialize_proto(original)142result = deserialize_proto(data, NestedProto)143144assert isinstance(result, NestedProto)145assert isinstance(result.simple, SimpleProto)146assert result.simple.name == "nested"147assert result.simple.value == 100148assert result.items == ["a", "b", "c"]149150def test_deserialize_datetime(self) -> None:151"""Test deserializing a proto with datetime fields."""152now = datetime(2024, 6, 15, 12, 0, 0, tzinfo=timezone.utc)153original = DateTimeProto(154created_at=now,155updated_at=now,156)157data = serialize_proto(original)158result = deserialize_proto(data, DateTimeProto)159160assert isinstance(result, DateTimeProto)161assert result.created_at == now162assert result.updated_at == now163164def test_deserialize_datetime_naive(self) -> None:165"""Test deserializing converts naive datetime to UTC."""166# Create a proto with a naive datetime167proto = DateTimeProto(168created_at=datetime(2024, 1, 1, 12, 0, 0), # Naive datetime169updated_at=None,170)171data = serialize_proto(proto)172result = deserialize_proto(data, DateTimeProto)173174# Result should have timezone-aware datetime175assert result.created_at.tzinfo is not None176177def test_deserialize_complex(self) -> None:178"""Test deserializing a complex proto."""179original = ComplexProto(180name="complex",181count=100,182score=0.95,183active=True,184tags=["tag1", "tag2"],185metadata={"key": "value"},186nested=SimpleProto(name="inner", value=50),187nested_list=[188SimpleProto(name="item1", value=1),189SimpleProto(name="item2", value=2),190],191timestamp=datetime(2024, 1, 1, 0, 0, 0, tzinfo=timezone.utc),192optional_time=None,193)194data = serialize_proto(original)195result = deserialize_proto(data, ComplexProto)196197assert result.name == "complex"198assert result.count == 100199assert result.score == 0.95200assert result.active is True201assert result.tags == ["tag1", "tag2"]202assert result.metadata == {"key": "value"}203assert result.nested.name == "inner"204assert len(result.nested_list) == 2205206def test_deserialize_raises_for_non_dataclass(self) -> None:207"""Test deserialize_proto raises TypeError for non-dataclass type."""208data = serialize_proto(SimpleProto(name="test", value=1))209with pytest.raises(TypeError, match="Expected dataclass type"):210deserialize_proto(data, dict)211212def test_deserialize_raises_for_invalid_data(self) -> None:213"""Test deserialize_proto raises ValueError for invalid data."""214with pytest.raises(ValueError, match="Failed to decode msgpack"):215deserialize_proto(b"invalid msgpack data", SimpleProto)216217218class TestRoundtripConversions:219"""Tests for roundtrip serialize/deserialize conversions."""220221def test_simple_roundtrip(self) -> None:222"""Test simple proto roundtrip."""223original = SimpleProto(name="roundtrip", value=999)224data = serialize_proto(original)225result = deserialize_proto(data, SimpleProto)226227assert result.name == original.name228assert result.value == original.value229230def test_empty_proto_roundtrip(self) -> None:231"""Test empty proto roundtrip."""232original = SimpleProto()233data = serialize_proto(original)234result = deserialize_proto(data, SimpleProto)235236assert result.name == ""237assert result.value == 0238239def test_unicode_roundtrip(self) -> None:240"""Test proto with unicode content roundtrip."""241original = SimpleProto(name="日本語テスト", value=42)242data = serialize_proto(original)243result = deserialize_proto(data, SimpleProto)244245assert result.name == "日本語テスト"246247def test_nested_list_roundtrip(self) -> None:248"""Test proto with nested list roundtrip."""249original = ComplexProto(250nested_list=[251SimpleProto(name="first", value=1),252SimpleProto(name="second", value=2),253SimpleProto(name="third", value=3),254],255)256data = serialize_proto(original)257result = deserialize_proto(data, ComplexProto)258259assert len(result.nested_list) == 3260assert result.nested_list[0].name == "first"261assert result.nested_list[2].value == 3262263264class TestComputeProtoHash:265"""Tests for compute_proto_hash function."""266267def test_compute_hash_returns_string(self) -> None:268"""Test compute_proto_hash returns a hex string."""269proto = SimpleProto(name="test", value=42)270hash_value = compute_proto_hash(proto)271272assert isinstance(hash_value, str)273assert len(hash_value) == 64 # SHA256 produces 64 hex chars274275def test_same_proto_same_hash(self) -> None:276"""Test identical protos produce identical hashes."""277proto1 = SimpleProto(name="test", value=42)278proto2 = SimpleProto(name="test", value=42)279280hash1 = compute_proto_hash(proto1)281hash2 = compute_proto_hash(proto2)282283assert hash1 == hash2284285def test_different_proto_different_hash(self) -> None:286"""Test different protos produce different hashes."""287proto1 = SimpleProto(name="test1", value=42)288proto2 = SimpleProto(name="test2", value=42)289290hash1 = compute_proto_hash(proto1)291hash2 = compute_proto_hash(proto2)292293assert hash1 != hash2294295def test_hash_deterministic(self) -> None:296"""Test hash is deterministic across multiple calls."""297proto = ComplexProto(298name="complex",299count=100,300tags=["a", "b", "c"],301nested=SimpleProto(name="inner", value=50),302)303304hashes = [compute_proto_hash(proto) for _ in range(5)]305assert all(h == hashes[0] for h in hashes)306307def test_hash_raises_for_non_dataclass(self) -> None:308"""Test compute_proto_hash raises TypeError for non-dataclass."""309with pytest.raises(TypeError, match="Expected dataclass instance"):310compute_proto_hash({"not": "dataclass"})311312313class TestRealProtoTypes:314"""Tests with actual proto types from the application."""315316def test_verse_reference_proto_roundtrip(self) -> None:317"""Test VerseReferenceProto serialization roundtrip."""318from src.protos.converters import VerseReferenceProto319320original = VerseReferenceProto(book="john", chapter=1, verse=1)321data = serialize_proto(original)322result = deserialize_proto(data, VerseReferenceProto)323324assert result.book == "john"325assert result.chapter == 1326assert result.verse == 1327328def test_translation_result_proto_roundtrip(self) -> None:329"""Test TranslationResultProto serialization roundtrip."""330from src.protos.converters import (331ConfidenceLevel,332TranslationResultProto,333VerseReferenceProto,334)335336original = TranslationResultProto(337reference=VerseReferenceProto(book="john", chapter=1, verse=1),338text="初めに言葉があった",339text_with_inline_furigana="初め[はじめ]に言葉[ことば]があった",340model="claude-3-opus",341confidence=ConfidenceLevel.HIGH,342confidence_score=0.95,343input_hash="abc123",344)345data = serialize_proto(original)346result = deserialize_proto(data, TranslationResultProto)347348assert result.text == "初めに言葉があった"349assert result.model == "claude-3-opus"350assert result.confidence_score == 0.95351assert result.input_hash == "abc123"352353def test_translation_prompt_input_proto_roundtrip(self) -> None:354"""Test TranslationPromptInputProto serialization roundtrip."""355from src.protos.converters import (356TranslationPromptInputProto,357VerseProto,358VerseReferenceProto,359)360361original = TranslationPromptInputProto(362target_verse=VerseProto(363reference=VerseReferenceProto(book="john", chapter=1, verse=1),364greek_text="Ἐν ἀρχῇ ἦν ὁ λόγος",365),366system_prompt="You are a translator",367user_prompt="Translate this verse",368model="claude-3-opus",369)370data = serialize_proto(original)371result = deserialize_proto(data, TranslationPromptInputProto)372373assert result.target_verse.greek_text == "Ἐν ἀρχῇ ἦν ὁ λόγος"374assert result.system_prompt == "You are a translator"375assert result.model == "claude-3-opus"376377def test_provenance_proto_roundtrip(self) -> None:378"""Test TranslationProvenanceProto serialization roundtrip."""379from datetime import datetime, timezone380381from src.protos.converters import TranslationProvenanceProto382383started = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)384completed = datetime(2024, 1, 1, 12, 0, 30, tzinfo=timezone.utc)385386original = TranslationProvenanceProto(387source_texts={"greek": "Greek text", "kjv": "KJV text"},388source_hashes={"greek": "hash1", "kjv": "hash2"},389model="claude-3-opus",390prompt_version="v1.0",391started_at=started,392completed_at=completed,393evaluation_score=85.5,394)395data = serialize_proto(original)396result = deserialize_proto(data, TranslationProvenanceProto)397398assert result.source_texts == {"greek": "Greek text", "kjv": "KJV text"}399assert result.model == "claude-3-opus"400assert result.started_at == started401assert result.completed_at == completed402assert result.evaluation_score == 85.5403404405class TestReconstructEdgeCases:406"""Tests for _reconstruct_dataclass edge branches."""407408def test_reconstruct_rejects_non_dict(self) -> None:409"""Non-dict input raises ValueError."""410from src.protos.serialization import _reconstruct_dataclass411412with pytest.raises(ValueError, match="Expected dict"):413_reconstruct_dataclass(["not", "a", "dict"], SimpleProto)414415def test_reconstruct_skips_unknown_fields(self) -> None:416"""Keys not present on the dataclass are ignored."""417from src.protos.serialization import _reconstruct_dataclass418419result = _reconstruct_dataclass(420{"name": "x", "value": 1, "unknown_field": "dropped"}, SimpleProto421)422assert result == SimpleProto(name="x", value=1)423424def test_reconstruct_unresolvable_string_annotation(self) -> None:425"""String annotations that can't be resolved pass values through."""426import typing as typing_mod427from unittest.mock import patch428429from src.protos.serialization import _reconstruct_dataclass430431fake_hints = {"name": "SomeUnresolvableType", "value": int}432with patch.object(typing_mod, "get_type_hints", return_value=fake_hints):433result = _reconstruct_dataclass({"name": "kept", "value": 2}, SimpleProto)434assert result.name == "kept"435assert result.value == 2436437def test_reconstruct_union_all_none_types(self) -> None:438"""Union whose non-None arms are exhausted leaves value as-is."""439from src.protos.serialization import _reconstruct_dataclass440441@dataclass442class OnlyNoneProto:443maybe: None = None444445result = _reconstruct_dataclass({"maybe": None}, OnlyNoneProto)446assert result.maybe is None447448def test_reconstruct_bare_list_annotation(self) -> None:449"""list annotation without item args keeps the raw list."""450from src.protos.serialization import _reconstruct_dataclass451452@dataclass453class BareListProto:454items: list = field(default_factory=list)455456result = _reconstruct_dataclass({"items": [1, "two", 3.0]}, BareListProto)457assert result.items == [1, "two", 3.0]458