Files
grm/tests/unit/test_check_translations.py
T

274 lines
10 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
import pytest
from click.testing import CliRunner
import scripts.ci.check_translations as check_translations
class TestExtractKeys:
def test_extracts_underscore_calls(self, tmp_path: Path) -> None:
f = tmp_path / "test.py"
f.write_text('from scripts.i18n import _\nprint(_("Hello world"))\n')
keys = check_translations.extract_keys(f)
assert "Hello world" in keys
def test_extracts_wrapper_calls(self, tmp_path: Path) -> None:
f = tmp_path / "test.py"
f.write_text('@_handle_errors("Update failed: {error}")\ndef foo(): pass\n')
keys = check_translations.extract_keys(f)
assert "Update failed: {error}" in keys
def test_ignores_non_string_args(self, tmp_path: Path) -> None:
f = tmp_path / "test.py"
f.write_text('x = "key"\n_(x)\n')
keys = check_translations.extract_keys(f)
assert keys == set()
def test_syntax_error_returns_empty(self, tmp_path: Path) -> None:
f = tmp_path / "test.py"
f.write_text("def broken(:\n")
keys = check_translations.extract_keys(f)
assert keys == set()
class TestCheckTranslationSet:
def test_all_good(self, tmp_path: Path) -> None:
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "mod.py").write_text('_("Hello")\n')
trans_file = tmp_path / "translations.json"
trans_file.write_text(
json.dumps({"Hello": {"en": "Hello", "bg": "Здравей", "de": "Hallo", "ru": "Привет", "zh": "你好"}})
)
result = check_translations.check_translation_set("test", src_dir, trans_file)
assert not result.errors
assert not result.warnings
def test_missing_key(self, tmp_path: Path) -> None:
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "mod.py").write_text('_("Missing")\n')
trans_file = tmp_path / "translations.json"
trans_file.write_text(json.dumps({"Other": {"en": "Other"}}))
result = check_translations.check_translation_set("test", src_dir, trans_file)
assert any("Missing key" in e for e in result.errors)
def test_dead_key(self, tmp_path: Path) -> None:
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "mod.py").write_text('_("Used")\n')
trans_file = tmp_path / "translations.json"
trans_file.write_text(json.dumps({"Used": {"en": "Used"}, "Dead": {"en": "Dead"}}))
result = check_translations.check_translation_set("test", src_dir, trans_file)
assert any("Dead key" in w for w in result.warnings)
def test_missing_language(self, tmp_path: Path) -> None:
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "mod.py").write_text('_("Hello")\n')
trans_file = tmp_path / "translations.json"
trans_file.write_text(json.dumps({"Hello": {"en": "Hello"}}))
result = check_translations.check_translation_set("test", src_dir, trans_file)
assert any("Missing languages" in w for w in result.warnings)
def test_missing_translations_file(self, tmp_path: Path) -> None:
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "mod.py").write_text('_("Hello")\n')
trans_file = tmp_path / "nonexistent.json"
result = check_translations.check_translation_set("test", src_dir, trans_file)
assert any("not found" in e for e in result.errors)
class TestMain:
def test_passes_on_clean_repo(self) -> None:
"""The actual repo should pass (with warnings for missing langs)."""
runner = CliRunner()
result = runner.invoke(check_translations.main, [])
assert result.exit_code == 0
def test_strict_fails_on_warnings(self) -> None:
"""--strict should fail if there are missing language warnings."""
runner = CliRunner()
result = runner.invoke(check_translations.main, ["--strict"])
# The repo has missing language warnings, so --strict should fail
assert result.exit_code == 1
def test_fails_on_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Should fail with exit code 1 when errors are found."""
error_result = check_translations.TranslationCheckResult(
name="mock",
src_dir=Path("/tmp"),
trans_file=Path("/tmp/t.json"),
errors=["Missing key: 'foo'"],
)
ok_result = check_translations.TranslationCheckResult(
name="mock2",
src_dir=Path("/tmp"),
trans_file=Path("/tmp/t.json"),
used_keys={"a"},
defined_keys={"a"},
)
monkeypatch.setattr(
check_translations,
"check_translation_set",
lambda name, src, trans: error_result if name == "GRM tool" else ok_result,
)
runner = CliRunner()
result = runner.invoke(check_translations.main, [])
assert result.exit_code == 1
assert "FAIL" in result.output
def test_passes_no_warnings(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Should pass with exit code 0 and 'PASS:' message when no warnings."""
ok_result = check_translations.TranslationCheckResult(
name="mock",
src_dir=Path("/tmp"),
trans_file=Path("/tmp/t.json"),
used_keys={"a"},
defined_keys={"a"},
)
monkeypatch.setattr(
check_translations,
"check_translation_set",
lambda name, src, trans: ok_result,
)
runner = CliRunner()
result = runner.invoke(check_translations.main, [])
assert result.exit_code == 0
assert "PASS: All translations" in result.output
class TestPrintResult:
def test_prints_all_good(self, capsys: pytest.CaptureFixture[str]) -> None:
result = check_translations.TranslationCheckResult(
name="test",
src_dir=Path("/tmp"),
trans_file=Path("/tmp/t.json"),
used_keys={"a"},
defined_keys={"a"},
)
check_translations.print_result(result)
captured = capsys.readouterr()
assert "All good!" in captured.out
def test_prints_errors(self, capsys: pytest.CaptureFixture[str]) -> None:
result = check_translations.TranslationCheckResult(
name="test",
src_dir=Path("/tmp"),
trans_file=Path("/tmp/t.json"),
errors=["Missing key: 'foo'"],
)
check_translations.print_result(result)
captured = capsys.readouterr()
assert "ERROR" in captured.err
def test_prints_warnings(self, capsys: pytest.CaptureFixture[str]) -> None:
result = check_translations.TranslationCheckResult(
name="test",
src_dir=Path("/tmp"),
trans_file=Path("/tmp/t.json"),
warnings=["Dead key: 'bar'"],
)
check_translations.print_result(result)
captured = capsys.readouterr()
assert "WARN" in captured.err
class TestScriptsI18n:
"""Test the scripts/i18n.py module."""
def test_english_default(self) -> None:
from scripts.i18n import _
assert _("Running tests...") == "Running tests..."
def test_format_kwargs(self) -> None:
from scripts.i18n import _
result = _("Comparing {base}..{head} ({count} files changed)", base="a", head="b", count=5)
assert "a..b" in result
assert "5 files" in result
def test_unknown_key_returns_key(self) -> None:
from scripts.i18n import _
assert _("Nonexistent key 12345") == "Nonexistent key 12345"
def test_grm_lang_override(self, monkeypatch: pytest.MonkeyPatch) -> None:
import importlib
monkeypatch.setenv("GRM_LANG", "de")
import scripts.i18n
importlib.reload(scripts.i18n)
# "ERROR: REPO_TOKEN is not set." has a German translation
result = scripts.i18n._("ERROR: REPO_TOKEN is not set.")
assert "FEHLER" in result
# Restore
monkeypatch.delenv("GRM_LANG", raising=False)
importlib.reload(scripts.i18n)
def test_unsupported_lang_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None:
import importlib
monkeypatch.setenv("GRM_LANG", "xx")
import scripts.i18n
importlib.reload(scripts.i18n)
result = scripts.i18n._("Running tests...")
assert result == "Running tests..." # Falls back to English
monkeypatch.delenv("GRM_LANG", raising=False)
importlib.reload(scripts.i18n)
class TestGrmI18nSeparation:
"""Test that GRM and CI translations are properly separated."""
def test_grm_translations_exist(self) -> None:
trans = Path("src/gitea_runner_manager/translations.json")
assert trans.exists()
data = json.loads(trans.read_text())
assert len(data) > 0
def test_ci_translations_exist(self) -> None:
trans = Path("scripts/translations.json")
assert trans.exists()
data = json.loads(trans.read_text())
assert len(data) > 0
def test_no_overlap_between_sets(self) -> None:
grm_data = json.loads(Path("src/gitea_runner_manager/translations.json").read_text())
ci_data = json.loads(Path("scripts/translations.json").read_text())
grm_keys = set(grm_data)
ci_keys = set(ci_data)
overlap = grm_keys & ci_keys
assert not overlap, f"Keys found in both translation sets: {overlap}"
def test_grm_translations_only_grm_keys(self) -> None:
"""GRM translations should not contain CI-only keys."""
grm_data = json.loads(Path("src/gitea_runner_manager/translations.json").read_text())
# These are CI-only keys that should NOT be in GRM translations
ci_only_keys = {"Running tests...", "Lint passed.", "Published to PyPI."}
for key in ci_only_keys:
assert key not in grm_data, f"CI key {key!r} found in GRM translations"
def test_ci_translations_only_ci_keys(self) -> None:
"""CI translations should not contain GRM-only keys."""
ci_data = json.loads(Path("scripts/translations.json").read_text())
# These are GRM-only keys that should NOT be in CI translations
grm_only_keys = {"Installing Gitea Runner on {host}", "SSH user", "NAME"}
for key in grm_only_keys:
assert key not in ci_data, f"GRM key {key!r} found in CI translations"