Files
computer-playbook/tests/unit/python/utils/i18n/test_libretranslate.py
T
kevinveenbirkenbachandClaude Opus 5 c325d1c852 fix(i18n): bake one model set so a new batch stops rebuilding the image
load_only reaches the image as a build ARG, and the RUN that fetches the
argos models is keyed on it. Pinning it to the languages a run happens to
translate therefore changed the ARG on every batch, invalidated that layer,
and refetched every model it names. Ten batches meant ten full rebuilds,
each one downloading what the previous had already downloaded.

Accumulating the languages instead would have been worse, not better: the
layer is all or nothing, so a growing set refetches the whole set each
time. What works is the opposite of following the batch. The lane now pins
load_only to every translatable code of both domains plus the pivot, 48 in
total, and asks for from_source, which drops the reverse pairs a lane that
only ever translates out of English never requests. The ARG is constant, so
the layer is built once and every later batch reuses it.

codes no longer reaches the rendered vars, which left it unused in deploy()
and server(); both lose it, along with the two call sites. That undoes
44260b8786, whose subject was passing it to a parameter this removes.

Correction to 4f2c9140fa: that message said an empty load_only makes the
image skip prefetch_models.py. It does not. The script always runs, and an
empty value only drops its --load_only_lang_codes filter, which is what
made it reach for every package. The consequence it described was real, the
mechanism was not.

Measured: after the one rebuild the server offers all 48 languages, and the
three batches since ran with no ansible task at all in their logs, going
straight to translating 12,518 entries across nineteen catalogs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 23:01:35 +02:00

228 lines
8.6 KiB
Python

import os
import subprocess
import unittest
import unittest.mock as mock
import urllib.error
from pathlib import Path
from tempfile import TemporaryDirectory
from utils.i18n.libretranslate import (
LibreTranslate,
accelerated,
deploying,
server,
)
class TestParallelDeployIsRefused(unittest.TestCase):
def test_a_running_deploy_stops_the_translation(self) -> None:
with (
mock.patch("utils.i18n.libretranslate.deploying", return_value=True),
mock.patch("utils.i18n.libretranslate.deploy") as started,
self.assertRaises(RuntimeError),
server(Path("/repo")),
):
pass
started.assert_not_called()
def test_a_live_pid_that_is_not_the_router_reports_no_deploy(self) -> None:
"""This process is alive and is not a deploy.
The guard used to answer yes to any live pid. Every sandboxed command
runs in its own pid namespace where the low numbers belong to that
sandbox, so a stale file naming pid 8 blocked every translation run.
"""
self.assertFalse(deploying(self._root_holding(os.getpid())))
def test_a_live_router_pid_reports_a_deploy(self) -> None:
root = Path(self._tmp.name)
router = root / "scripts" / "deploy" / "main.sh"
router.parent.mkdir(parents=True, exist_ok=True)
router.write_text("echo started\nread -r _\n", encoding="utf-8")
held = subprocess.Popen(
["bash", str(router)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
self.addCleanup(held.kill)
self.addCleanup(held.stdout.close)
self.assertEqual(
held.stdout.readline(),
b"started\n",
"the router has to reach its first line before its command line is read; "
"/proc/<pid>/cmdline is empty until the child execs, which it is for "
"376 of 400 reads taken right after Popen returns",
)
self.assertTrue(deploying(self._root_holding(held.pid)))
def test_a_stale_pid_file_reports_no_deploy(self) -> None:
root = self._root_holding(4242)
with mock.patch("os.kill", side_effect=ProcessLookupError):
self.assertFalse(deploying(root))
def test_a_missing_pid_file_reports_no_deploy(self) -> None:
self.assertFalse(deploying(Path(self._tmp.name)))
def _root_holding(self, pid: int) -> Path:
root = Path(self._tmp.name)
(root / "build").mkdir(exist_ok=True)
(root / "build" / "deploy.pid").write_text(str(pid))
return root
def setUp(self) -> None:
self._tmp = TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
class TestTranslateFailures(unittest.TestCase):
def setUp(self) -> None:
self.client = LibreTranslate("http://libretranslate", 1)
def test_an_unreachable_server_stops_the_run(self) -> None:
refused = urllib.error.URLError(ConnectionRefusedError())
with (
mock.patch.object(self.client, "_call", side_effect=refused),
self.assertRaises(OSError),
):
self.client.translate(["Hello", "World"], "de")
def test_a_rejected_request_discards_only_its_texts(self) -> None:
rejected = urllib.error.HTTPError(
"http://libretranslate", 500, "boom", {}, None
)
with (
mock.patch("utils.i18n.libretranslate.time.sleep"),
mock.patch.object(self.client, "_call", side_effect=rejected),
):
self.assertEqual(
self.client.translate(["Hello", "World"], "de").values, [None, None]
)
class TestRefusalsAreRetried(unittest.TestCase):
"""A server that buckles under the lanes must not cost entries for good."""
def setUp(self) -> None:
self.client = LibreTranslate("http://libretranslate", 1)
self.refusal = urllib.error.HTTPError(
"http://libretranslate", 500, "boom", {}, None
)
def test_a_transient_refusal_is_retried_until_it_succeeds(self) -> None:
answers = [self.refusal, self.refusal, {"translatedText": ["Hallo"]}]
with (
mock.patch("utils.i18n.libretranslate.time.sleep"),
mock.patch.object(self.client, "_call", side_effect=answers),
):
outcome = self.client.translate(["Hello"], "de")
self.assertEqual(outcome.values, ["Hallo"])
self.assertEqual(outcome.refused, 2)
self.assertEqual(outcome.damaged, 0)
def test_a_permanent_refusal_is_counted_and_named(self) -> None:
with (
mock.patch("utils.i18n.libretranslate.time.sleep"),
mock.patch.object(self.client, "_call", side_effect=self.refusal),
):
outcome = self.client.translate(["Hello"], "de")
self.assertEqual(outcome.values, [None])
self.assertTrue(outcome.refused)
self.assertIn("HTTPError", outcome.refusal)
def test_a_catalog_never_inherits_another_lane_s_counts(self) -> None:
answers = [
self.refusal,
{"translatedText": ["Hallo"]},
{"translatedText": ["Hi"]},
]
with (
mock.patch("utils.i18n.libretranslate.time.sleep"),
mock.patch.object(self.client, "_call", side_effect=answers),
):
first = self.client.translate(["Hello"], "de")
second = self.client.translate(["Hello"], "de")
self.assertEqual(first.refused, 1)
self.assertEqual(second.refused, 0)
class TestScriptedTargetCodes(unittest.TestCase):
"""LibreTranslate offers Chinese as zh-Hans/zh-Hant, never as bare zh."""
def setUp(self) -> None:
self.client = LibreTranslate("http://libretranslate", 1)
self.served = [{"code": "en", "targets": ["de", "zh-Hans", "zh-Hant"]}]
def test_the_catalog_code_is_sent_as_the_code_the_server_serves(self) -> None:
with mock.patch.object(
self.client, "_call", return_value={"translatedText": ["你好"]}
) as call:
self.client.translate(["Hello"], "zh")
self.assertEqual(call.call_args.args[1]["target"], "zh-Hans")
def test_waiting_accepts_a_server_that_only_names_the_script(self) -> None:
with mock.patch.object(self.client, "_call", return_value=self.served):
self.client.wait(["zh", "de"], timeout=0)
def test_waiting_still_reports_a_target_nobody_serves(self) -> None:
with (
mock.patch.object(self.client, "_call", return_value=self.served),
self.assertRaises(TimeoutError) as raised,
):
self.client.wait(["fr"], timeout=0)
self.assertIn("fr", str(raised.exception))
class TestAccelerated(unittest.TestCase):
def setUp(self) -> None:
patched = mock.patch.dict(os.environ, {}, clear=False)
patched.start()
os.environ.pop("INFINITO_GPU_COUNT", None)
self.addCleanup(patched.stop)
def _runtimes(self, stdout: str) -> mock.Mock:
return mock.Mock(spec=subprocess.CompletedProcess, stdout=stdout)
def test_the_reserved_count_overrides_the_daemon(self) -> None:
listed = self._runtimes('{"nvidia":{"path":"nvidia-container-runtime"}}')
os.environ["INFINITO_GPU_COUNT"] = "0"
with mock.patch("subprocess.run", return_value=listed):
self.assertFalse(accelerated())
def test_a_reserved_count_enables_the_gpu_without_asking_the_daemon(self) -> None:
os.environ["INFINITO_GPU_COUNT"] = "all"
with mock.patch("subprocess.run", side_effect=AssertionError("asked docker")):
self.assertTrue(accelerated())
def test_an_empty_count_still_asks_the_daemon(self) -> None:
listed = self._runtimes('{"nvidia":{"path":"nvidia-container-runtime"}}')
os.environ["INFINITO_GPU_COUNT"] = " "
with mock.patch("subprocess.run", return_value=listed):
self.assertTrue(accelerated())
def test_a_registered_nvidia_runtime_enables_the_gpu(self) -> None:
listed = self._runtimes('{"nvidia":{"path":"nvidia-container-runtime"}}')
with mock.patch("subprocess.run", return_value=listed):
self.assertTrue(accelerated())
def test_a_host_without_the_toolkit_falls_back_to_the_cpu(self) -> None:
listed = self._runtimes('{"runc":{"path":"runc"}}')
with mock.patch("subprocess.run", return_value=listed):
self.assertFalse(accelerated())
def test_an_unavailable_docker_falls_back_to_the_cpu(self) -> None:
with mock.patch("subprocess.run", return_value=self._runtimes("")):
self.assertFalse(accelerated())
if __name__ == "__main__":
unittest.main()