- Prefer curated character names over cached or generated translations - Preserve name spellings inside compound labels and parsed speakers - Reload vocab before runs and refine default event-code handling - Add regression coverage for glossary precedence and matching
131 lines
4.7 KiB
Python
131 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Regression tests for deterministic RPG Maker speaker glossary handling."""
|
|
|
|
import unittest
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from unittest.mock import patch
|
|
|
|
import modules.rpgmakermvmz as mvmz
|
|
from util.translation import buildMatchedVocabText, parseVocabWithCategories
|
|
|
|
|
|
VOCAB = (
|
|
"# Game Characters\n"
|
|
"ユウ (Yuu) - Male protagonist.\n"
|
|
"ハルカ (Haruka) - Female heroine.\n\n"
|
|
"# Terms\n"
|
|
"教室 (Classroom)\n"
|
|
)
|
|
|
|
|
|
class MVMZSpeakerVocabTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.original_vocab = mvmz.VOCAB
|
|
self.original_config_vocab = mvmz.TRANSLATION_CONFIG.vocab
|
|
self.original_names = mvmz.NAMESLIST
|
|
self.original_collected = mvmz.SPEAKER_COLLECTED
|
|
self.original_parse_mode = mvmz.SPEAKER_PARSE_MODE
|
|
self.original_preflight = mvmz.PREFLIGHT_COUNT_MODE
|
|
mvmz.VOCAB = VOCAB
|
|
mvmz.NAMESLIST = []
|
|
mvmz.SPEAKER_COLLECTED = []
|
|
mvmz.SPEAKER_PARSE_MODE = False
|
|
mvmz.PREFLIGHT_COUNT_MODE = False
|
|
with mvmz._speakerCacheLock:
|
|
mvmz._speakerCache.clear()
|
|
|
|
def tearDown(self):
|
|
mvmz.VOCAB = self.original_vocab
|
|
mvmz.TRANSLATION_CONFIG.vocab = self.original_config_vocab
|
|
mvmz.NAMESLIST = self.original_names
|
|
mvmz.SPEAKER_COLLECTED = self.original_collected
|
|
mvmz.SPEAKER_PARSE_MODE = self.original_parse_mode
|
|
mvmz.PREFLIGHT_COUNT_MODE = self.original_preflight
|
|
with mvmz._speakerCacheLock:
|
|
mvmz._speakerCache.clear()
|
|
|
|
def test_exact_vocab_name_bypasses_model_and_stale_cache(self):
|
|
with mvmz._speakerCacheLock:
|
|
mvmz._speakerCache["ユウ"] = "Yu"
|
|
|
|
with patch.object(mvmz, "translateAI") as translate:
|
|
result = mvmz.getSpeaker("ユウ")
|
|
|
|
self.assertEqual(result, ["Yuu", [0, 0]])
|
|
translate.assert_not_called()
|
|
|
|
def test_character_name_is_substituted_inside_compound_label(self):
|
|
def translate(text, _context, _batch=False):
|
|
self.assertEqual(text, "Yuuイベント5")
|
|
return ["Yuu Event 5", [3, 2]]
|
|
|
|
with patch.object(mvmz, "translateAI", side_effect=translate):
|
|
result = mvmz.getSpeaker("ユウイベント5")
|
|
|
|
self.assertEqual(result, ["Yuu Event 5", [3, 2]])
|
|
|
|
def test_game_characters_override_generated_speaker_spelling(self):
|
|
mvmz.VOCAB = (
|
|
"# Game Characters\nユウ (Yuu) - Male protagonist.\n\n"
|
|
"# Speakers\nユウ (Yu)\n"
|
|
)
|
|
|
|
with patch.object(mvmz, "translateAI") as translate:
|
|
result = mvmz.getSpeaker("ユウ")
|
|
|
|
self.assertEqual(result, ["Yuu", [0, 0]])
|
|
translate.assert_not_called()
|
|
|
|
def test_reload_vocab_updates_translation_config(self):
|
|
with TemporaryDirectory() as tmp:
|
|
vocab_path = Path(tmp) / "vocab.txt"
|
|
vocab_path.write_text("# Game Characters\nユウ (You)\n", encoding="utf-8")
|
|
with patch.object(mvmz, "VOCAB_PATH", vocab_path):
|
|
mvmz._reload_vocab()
|
|
|
|
self.assertIn("ユウ (You)", mvmz.VOCAB)
|
|
self.assertEqual(mvmz.TRANSLATION_CONFIG.vocab, mvmz.VOCAB)
|
|
|
|
def test_parse_speakers_reuses_curated_name(self):
|
|
mvmz.SPEAKER_PARSE_MODE = True
|
|
mvmz.SPEAKER_COLLECTED = ["ユウ"]
|
|
with TemporaryDirectory() as tmp:
|
|
vocab_path = Path(tmp) / "vocab.txt"
|
|
vocab_path.write_text(VOCAB, encoding="utf-8")
|
|
with (
|
|
patch.object(mvmz, "VOCAB_PATH", vocab_path),
|
|
patch.object(mvmz, "translateAI") as translate,
|
|
):
|
|
mvmz.finalizeSpeakerParse()
|
|
written = vocab_path.read_text(encoding="utf-8")
|
|
|
|
translate.assert_not_called()
|
|
self.assertIn("# Speakers\nユウ (Yuu)", written)
|
|
|
|
|
|
class CharacterCompoundMatchingTests(unittest.TestCase):
|
|
def test_character_entry_matches_inside_katakana_compound(self):
|
|
pairs = parseVocabWithCategories(VOCAB)
|
|
matched = buildMatchedVocabText(pairs, "ユウイベント5")
|
|
|
|
self.assertIn("ユウ (Yuu)", matched)
|
|
|
|
def test_non_character_short_term_keeps_script_boundaries(self):
|
|
pairs = parseVocabWithCategories("# Terms\nキス (Kiss)\n")
|
|
matched = buildMatchedVocabText(pairs, "テキスト")
|
|
|
|
self.assertEqual(matched, "")
|
|
|
|
def test_curated_character_spelling_suppresses_generated_conflict(self):
|
|
pairs = parseVocabWithCategories(
|
|
"# Game Characters\nユウ (Yuu)\n\n# Speakers\nユウ (Yu)\n"
|
|
)
|
|
matched = buildMatchedVocabText(pairs, "ユウイベント5")
|
|
|
|
self.assertIn("ユウ (Yuu)", matched)
|
|
self.assertNotIn("ユウ (Yu)", matched)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|