mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-19 21:26:47 +00:00
198 lines
11 KiB
Python
198 lines
11 KiB
Python
"""Offline regression tests for the Monitor catalog generator."""
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
from contextlib import redirect_stdout, redirect_stderr
|
|
|
|
SCRIPT = Path(__file__).parents[1] / "build_i18n_messages.py"
|
|
SPEC = importlib.util.spec_from_file_location("build_i18n_messages", SCRIPT)
|
|
MODULE = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(MODULE)
|
|
|
|
|
|
class I18nMessagesTests(unittest.TestCase):
|
|
def run_generator(self, source, target, *options, provider=None):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
en = root / "en" / "common.json"
|
|
it = root / "it" / "common.json"
|
|
en.parent.mkdir()
|
|
it.parent.mkdir()
|
|
en.write_text(json.dumps(source), encoding="utf-8")
|
|
it.write_text(json.dumps(target), encoding="utf-8")
|
|
before = it.read_bytes()
|
|
argv = [str(SCRIPT), "--source", str(en), "--messages-dir", str(root),
|
|
"--languages", "it", "--sleep", "0", *options]
|
|
output = io.StringIO()
|
|
with mock.patch.object(sys, "argv", argv), mock.patch.object(
|
|
MODULE, "translate_one", side_effect=provider or (lambda text, *args: "IT " + text)
|
|
) as translate, redirect_stdout(output), redirect_stderr(output):
|
|
result = MODULE.main()
|
|
return result, json.loads(it.read_text()), translate.call_args_list, output.getvalue(), before == it.read_bytes()
|
|
|
|
def test_generator_fills_array_leaves_preserving_existing_values(self):
|
|
source = {"items": ["One", {"title": "Two", "children": ["Three"]}],
|
|
"empty": [], "object": {}, "enabled": True, "count": 2, "none": None}
|
|
target = {"items": ["Uno"], "extra": {"0": "Keep"}}
|
|
result, actual, calls, _, _ = self.run_generator(source, target, "--save-every", "1")
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(actual, {**source, "items": ["Uno", {"title": "IT Two", "children": ["IT Three"]}],
|
|
"extra": {"0": "Keep"}})
|
|
self.assertEqual([call.args[0] for call in calls], ["Two", "Three"])
|
|
|
|
def test_wrong_types_defer_locale_without_calls_or_writes_even_on_refresh(self):
|
|
source = {"new": "New", "terminal": {"commandDescriptions": ["One", "Two"]}}
|
|
for malformed in ["['Uno', 'Due']", '["Uno", "Due"]', "ambiguous human text", {}, None, 7]:
|
|
for options in [(), ("--refresh",)]:
|
|
with self.subTest(malformed=malformed, options=options):
|
|
target = {"terminal": {"commandDescriptions": malformed}, "human": "Keep"}
|
|
result, actual, calls, output, unchanged = self.run_generator(source, target, *options)
|
|
self.assertEqual(result, 1)
|
|
self.assertEqual(actual, target)
|
|
self.assertTrue(unchanged)
|
|
self.assertEqual(calls, [])
|
|
self.assertIn("/terminal/commandDescriptions", output)
|
|
self.assertIn("manual", output)
|
|
|
|
def test_structure_only_catalog_is_written_without_translation(self):
|
|
source = {"items": ["", [], {}, None, False, 0]}
|
|
result, actual, calls, _, _ = self.run_generator(source, {})
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(actual, source)
|
|
self.assertEqual(calls, [])
|
|
|
|
def test_limit_keeps_array_positions_as_null_fallbacks(self):
|
|
result, actual, calls, _, _ = self.run_generator(
|
|
{"items": ["One", "", "Three", "Four"]}, {}, "--limit", "1")
|
|
self.assertEqual(actual, {"items": ["IT One", "", None, None]})
|
|
self.assertEqual(len(calls), 1)
|
|
|
|
def test_existing_nonempty_including_english_and_stale_text_is_unchanged(self):
|
|
source = {"items": ["Changed English", "CPU", "Three"]}
|
|
target = {"items": ["Human translation", "CPU", "Tre", "Extra"]}
|
|
result, actual, calls, _, unchanged = self.run_generator(source, target)
|
|
self.assertEqual(actual, target)
|
|
self.assertEqual(calls, [])
|
|
self.assertTrue(unchanged)
|
|
|
|
def test_second_run_retries_null_placeholders_preserving_translations(self):
|
|
source = {"items": ["One", "Two", "", "Three", "Four"]}
|
|
target = {"items": ["Human"], "extra": "Keep"}
|
|
result, first, calls, _, _ = self.run_generator(source, target, "--limit", "1")
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(first, {"items": ["Human", "IT Two", "", None, None], "extra": "Keep"})
|
|
self.assertEqual([call.args[0] for call in calls], ["Two"])
|
|
result, second, calls, _, _ = self.run_generator(source, first, "--save-every", "1")
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(second, {"items": ["Human", "IT Two", "", "IT Three", "IT Four"], "extra": "Keep"})
|
|
self.assertEqual([call.args[0] for call in calls], ["Three", "Four"])
|
|
|
|
def test_refresh_and_provider_failure_keep_existing_fallback_semantics(self):
|
|
def provider(text, *args):
|
|
if text == "Two":
|
|
raise RuntimeError("offline fixture failure")
|
|
return "IT " + text
|
|
result, actual, calls, output, _ = self.run_generator(
|
|
{"items": ["One", "Two", "Three"]}, {"items": ["Uno", "Due"]},
|
|
"--refresh", "--save-every", "1", provider=provider)
|
|
self.assertEqual(result, 0)
|
|
self.assertEqual(actual, {"items": ["IT One", "Due", "IT Three"]})
|
|
self.assertEqual(len(calls), 3)
|
|
self.assertIn("1 translation failures", output)
|
|
|
|
def test_null_and_empty_string_are_filled_and_tokens_restored(self):
|
|
source = {"items": ["Hello {vmid}", "Use Proxmox"]}
|
|
result, actual, calls, _, _ = self.run_generator(source, {"items": [None, ""]})
|
|
self.assertEqual(actual, {"items": ["IT Hello {vmid}", "IT Use Proxmox"]})
|
|
self.assertNotIn("{vmid}", calls[0].args[0])
|
|
self.assertNotIn("Proxmox", calls[1].args[0])
|
|
|
|
def test_root_pointer_is_empty_not_empty_key_pointer(self):
|
|
for source, target, pointer in [({"a": "Text"}, [], 'JSON Pointer "" (root)'),
|
|
({"": []}, {"": "Text"}, 'JSON Pointer "/"'),
|
|
({"a~/": []}, {"a~/": "Text"}, 'JSON Pointer "/a~0~1"')]:
|
|
with self.subTest(pointer=pointer):
|
|
result, _, calls, output, unchanged = self.run_generator(source, target)
|
|
self.assertEqual(result, 1)
|
|
self.assertEqual(calls, [])
|
|
self.assertTrue(unchanged)
|
|
self.assertIn(pointer + ": expected", output)
|
|
|
|
def test_wrong_scalar_and_nested_types_are_rejected(self):
|
|
for source, target in [({"a": "Text"}, {"a": 42}),
|
|
({"a": [{"b": "Text"}]}, {"a": ["human"]}),
|
|
({"a": False}, {"a": 0}),
|
|
({"a": {}}, {"a": []})]:
|
|
with self.subTest(source=source):
|
|
result, actual, calls, _, unchanged = self.run_generator(source, target)
|
|
self.assertEqual(result, 1)
|
|
self.assertTrue(unchanged)
|
|
self.assertEqual(calls, [])
|
|
|
|
def test_real_english_array_round_trip_and_generation(self):
|
|
catalog = json.loads((SCRIPT.parents[2] / "AppImage/messages/en/common.json").read_text())
|
|
descriptions = catalog["terminal"]["commandDescriptions"]
|
|
self.assertIsInstance(descriptions, list)
|
|
self.assertTrue(descriptions)
|
|
self.assertEqual(MODULE.unflatten(MODULE.flatten(catalog)), catalog)
|
|
result, actual, calls, _, _ = self.run_generator({"terminal": {"commandDescriptions": descriptions}}, {})
|
|
self.assertEqual(len(calls), sum(isinstance(text, str) and bool(text) for text in descriptions))
|
|
self.assertEqual(actual["terminal"]["commandDescriptions"], ["IT " + text for text in descriptions])
|
|
|
|
def test_preflight_skips_invalid_locales_but_processes_valid_locales(self):
|
|
for options in [(), ("--refresh",)]:
|
|
with self.subTest(options=options), tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
catalogs = {"en": {"items": ["One", "Two"]},
|
|
"sk": {"items": ["Human"]},
|
|
"it": {"items": "Uno"}, "de": {"items": {}}}
|
|
for lang, data in catalogs.items():
|
|
path = root / lang / "common.json"
|
|
path.parent.mkdir()
|
|
path.write_text(json.dumps(data))
|
|
before = {lang: (root / lang / "common.json").read_bytes() for lang in catalogs}
|
|
reads = []
|
|
real_read = MODULE.read_json
|
|
|
|
def read(path):
|
|
reads.append(path.parent.name)
|
|
return real_read(path)
|
|
|
|
def translate(text, lang, *args):
|
|
self.assertEqual(reads, ["en", "sk", "it", "de"])
|
|
self.assertEqual(lang, "sk")
|
|
return "SK " + text
|
|
|
|
argv = [str(SCRIPT), "--source", str(root / "en/common.json"),
|
|
"--messages-dir", str(root), "--languages", "sk,it,de",
|
|
"--sleep", "0", "--save-every", "1", *options]
|
|
output = io.StringIO()
|
|
with mock.patch.object(sys, "argv", argv), \
|
|
mock.patch.object(MODULE, "read_json", side_effect=read), \
|
|
mock.patch.object(MODULE, "translate_one", side_effect=translate) as provider, \
|
|
mock.patch.object(MODULE, "write_json", wraps=MODULE.write_json) as writer, \
|
|
redirect_stderr(output), redirect_stdout(output):
|
|
self.assertEqual(MODULE.main(), 1)
|
|
self.assertEqual(json.loads((root / "sk/common.json").read_text()),
|
|
{"items": ["SK One" if options else "Human", "SK Two"]})
|
|
self.assertEqual(provider.call_count, 2 if options else 1)
|
|
self.assertTrue(all(call.args[0].parent.name == "sk" for call in writer.call_args_list))
|
|
for lang in ["en", "it", "de"]:
|
|
self.assertEqual((root / lang / "common.json").read_bytes(), before[lang])
|
|
self.assertIn("Partial failure: skipped 2 locale(s): it, de", output.getvalue())
|
|
self.assertNotIn("generated successfully", output.getvalue())
|
|
|
|
def test_round_trip_preserves_json_types_and_literal_keys(self):
|
|
catalog = {"terminal": {"commandDescriptions": ["One", {"nested": ["Two", [], {}]}]},
|
|
"0": {"a.b": "literal", "": None}, "flags": [True, False, 2, 1.5]}
|
|
self.assertEqual(MODULE.unflatten(MODULE.flatten(catalog)), catalog)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|