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"