263 lines
9.7 KiB
Python
263 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Unit tests for util/wolfdawn/inject.py orchestration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
os.chdir(ROOT)
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from util.wolfdawn import inject as wi # noqa: E402
|
|
|
|
|
|
class ListInjectableTests(unittest.TestCase):
|
|
def test_lists_translated_files_with_manifest_entries(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
translated = root / "translated"
|
|
translated.mkdir()
|
|
(translated / "names.json").write_text("{}", encoding="utf-8")
|
|
(translated / "Map001.mps.json").write_text("{}", encoding="utf-8")
|
|
(translated / "orphan.json").write_text("{}", encoding="utf-8")
|
|
entries = [
|
|
{"json": "names.json", "kind": "names", "base": "/data"},
|
|
{"json": "Map001.mps.json", "kind": "map", "base": "/data/Map001.mps"},
|
|
]
|
|
self.assertEqual(
|
|
wi.list_injectable(translated, entries),
|
|
["Map001.mps.json", "names.json"],
|
|
)
|
|
|
|
|
|
class InjectOrderTests(unittest.TestCase):
|
|
def test_names_runs_before_strings(self):
|
|
calls: list[str] = []
|
|
string_bases: list[str] = []
|
|
|
|
def fake_names(*_a, **_k):
|
|
calls.append("names")
|
|
return mock.Mock(
|
|
ok=True,
|
|
returncode=0,
|
|
stdout="applied 2 name change(s) (0 drifted/unmatched)",
|
|
stderr="",
|
|
)
|
|
|
|
def fake_strings(edited_json, base, output, **_k):
|
|
calls.append("strings")
|
|
string_bases.append(str(base))
|
|
return mock.Mock(
|
|
ok=True,
|
|
returncode=0,
|
|
stdout="applied 1 translation(s) (0 drifted)",
|
|
stderr="",
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
translated = root / "translated"
|
|
originals = root / "originals"
|
|
data = root / "data"
|
|
translated.mkdir()
|
|
originals.mkdir()
|
|
data.mkdir()
|
|
(translated / "names.json").write_text("{}", encoding="utf-8")
|
|
(translated / "Map001.mps.json").write_text(
|
|
'{"kind":"map","scenes":[]}', encoding="utf-8"
|
|
)
|
|
map_live = data / "Map001.mps"
|
|
map_orig = originals / "Map001.mps"
|
|
map_live.write_bytes(b"live")
|
|
map_orig.write_bytes(b"orig")
|
|
|
|
entries = [
|
|
{"json": "names.json", "kind": "names", "base": str(data)},
|
|
{"json": "Map001.mps.json", "kind": "map", "base": str(map_live)},
|
|
]
|
|
|
|
def would_apply_counts():
|
|
yield 1 # prepare: after restore
|
|
yield 1 # _inject_names: preflight
|
|
yield 0 # _inject_names: post-check
|
|
|
|
counts = would_apply_counts()
|
|
|
|
def next_would_apply(*_a, **_k):
|
|
return next(counts, 0)
|
|
|
|
with mock.patch.object(wi.wolf_originals, "names_inject_would_apply", side_effect=next_would_apply), \
|
|
mock.patch.object(wi.wolfdawn, "names_inject", side_effect=fake_names), \
|
|
mock.patch.object(wi.wolfdawn, "strings_inject", side_effect=fake_strings):
|
|
report = wi.inject_selected(
|
|
["Map001.mps.json", wi.NAMES_JSON],
|
|
manifest_entries=entries,
|
|
data_dir=data,
|
|
originals_dir=originals,
|
|
translated_dir=translated,
|
|
game_root=data.parent,
|
|
)
|
|
|
|
self.assertEqual(calls, ["names", "strings"])
|
|
self.assertTrue(report.ok)
|
|
self.assertEqual(len(report.succeeded), 2)
|
|
# After names-inject, strings must use live Data/ as --base so
|
|
# name-only fields are not rebuilt from pristine Japanese originals.
|
|
self.assertEqual(string_bases, [str(map_live)])
|
|
|
|
def test_strings_alone_use_pristine_original_base(self):
|
|
string_bases: list[str] = []
|
|
string_drift: list[bool] = []
|
|
|
|
def fake_strings(edited_json, base, output, **kwargs):
|
|
string_bases.append(str(base))
|
|
string_drift.append(bool(kwargs.get("allow_code_drift")))
|
|
return mock.Mock(
|
|
ok=True,
|
|
returncode=0,
|
|
stdout="applied 1 translation(s) (0 drifted)",
|
|
stderr="",
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
translated = root / "translated"
|
|
originals = root / "originals"
|
|
data = root / "data"
|
|
translated.mkdir()
|
|
originals.mkdir()
|
|
data.mkdir()
|
|
(translated / "Map001.mps.json").write_text(
|
|
'{"kind":"map","scenes":[]}', encoding="utf-8"
|
|
)
|
|
map_live = data / "Map001.mps"
|
|
map_orig = originals / "Map001.mps"
|
|
map_live.write_bytes(b"live")
|
|
map_orig.write_bytes(b"orig")
|
|
entries = [
|
|
{"json": "Map001.mps.json", "kind": "map", "base": str(map_live)},
|
|
]
|
|
with mock.patch.object(wi.wolfdawn, "strings_inject", side_effect=fake_strings):
|
|
report = wi.inject_selected(
|
|
["Map001.mps.json"],
|
|
manifest_entries=entries,
|
|
data_dir=data,
|
|
originals_dir=originals,
|
|
translated_dir=translated,
|
|
game_root=data.parent,
|
|
)
|
|
self.assertTrue(report.ok)
|
|
self.assertEqual(string_bases, [str(map_orig)])
|
|
self.assertEqual(string_drift, [False])
|
|
|
|
def test_strings_auto_allow_code_drift_for_font_size(self):
|
|
string_drift: list[bool] = []
|
|
|
|
def fake_strings(edited_json, base, output, **kwargs):
|
|
string_drift.append(bool(kwargs.get("allow_code_drift")))
|
|
return mock.Mock(
|
|
ok=True,
|
|
returncode=0,
|
|
stdout="applied 1 translation(s) (0 drifted)",
|
|
stderr="",
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
translated = root / "translated"
|
|
originals = root / "originals"
|
|
data = root / "data"
|
|
translated.mkdir()
|
|
originals.mkdir()
|
|
data.mkdir()
|
|
doc = {
|
|
"kind": "db",
|
|
"groups": [
|
|
{
|
|
"typeName": "噂",
|
|
"lines": [
|
|
{
|
|
"source": r"\c[21]\f[20]娼館\c[19]\f[18]",
|
|
"text": r"\f[14]\c[21]\f[16]Brothel\c[19]\f[14]",
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
import json
|
|
|
|
(translated / "DataBase.project.json").write_text(
|
|
json.dumps(doc, ensure_ascii=False), encoding="utf-8"
|
|
)
|
|
proj_live = data / "DataBase.project"
|
|
proj_orig = originals / "DataBase.project"
|
|
proj_live.write_bytes(b"live")
|
|
proj_orig.write_bytes(b"orig")
|
|
(data / "DataBase.dat").write_bytes(b"live-dat")
|
|
(originals / "DataBase.dat").write_bytes(b"orig-dat")
|
|
entries = [
|
|
{
|
|
"json": "DataBase.project.json",
|
|
"kind": "db",
|
|
"base": str(proj_live),
|
|
},
|
|
]
|
|
with mock.patch.object(wi.wolfdawn, "strings_inject", side_effect=fake_strings):
|
|
report = wi.inject_selected(
|
|
["DataBase.project.json"],
|
|
manifest_entries=entries,
|
|
data_dir=data,
|
|
originals_dir=originals,
|
|
translated_dir=translated,
|
|
game_root=data.parent,
|
|
allow_code_drift=False,
|
|
)
|
|
self.assertTrue(report.ok)
|
|
self.assertEqual(string_drift, [True])
|
|
|
|
def test_names_result_mentions_safety_skips(self):
|
|
res = mock.Mock(
|
|
ok=True,
|
|
returncode=0,
|
|
stdout=(
|
|
"would apply 10 name change(s) (0 drifted/unmatched)\n"
|
|
"WARNING: 46 line(s) left UNTRANSLATED by a safety guard - "
|
|
"46 control-code mismatch, 0 not encodable\n"
|
|
),
|
|
stderr="",
|
|
)
|
|
# parse_names_inject_counts needs "applied N name change"
|
|
res.stdout = (
|
|
"applied 10 name change(s) (0 drifted/unmatched)\n"
|
|
"WARNING: 46 line(s) left UNTRANSLATED by a safety guard - "
|
|
"46 control-code mismatch, 0 not encodable\n"
|
|
)
|
|
result = wi._interpret_names_result("names.json", res)
|
|
self.assertTrue(result.success)
|
|
self.assertIn("46 skipped by safety guard", result.summary)
|
|
|
|
|
|
class ReportDialogTests(unittest.TestCase):
|
|
def test_failure_dialog_lists_every_problem(self):
|
|
report = wi.InjectReport(
|
|
files=[
|
|
wi.FileInjectResult("a.json", True, "applied 3 line(s)"),
|
|
wi.FileInjectResult("b.json", False, "0 applied, 2 drifted", detail="stderr"),
|
|
],
|
|
sync_failures=[("c.json", "permission denied")],
|
|
)
|
|
title, body = wi.format_report_dialog(report)
|
|
self.assertIn("errors", title)
|
|
self.assertIn("✗ b.json", body)
|
|
self.assertIn("✗ sync c.json", body)
|
|
self.assertIn("✓ a.json", body)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|