274 lines
10 KiB
Python
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"
|