from __future__ import annotations import json from pathlib import Path import pytest from click.testing import CliRunner import devx.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 devx.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" all_langs = {"en": "Hello", "bg": "Здравей", "de": "Hallo", "ru": "Привет", "zh": "你好", "pl": "Cześć"} # Include dynamic keys since collect_keys now adds them for all dirs data = {"Hello": all_langs} for dk in check_translations.DYNAMIC_KEYS: data[dk] = all_langs trans_file.write_text(json.dumps(data)) 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 e for e in result.errors) 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 e for e in result.errors) 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, monkeypatch: pytest.MonkeyPatch) -> None: """The actual repo should pass when there are no errors.""" ok_result = check_translations.TranslationCheckResult( name="devx", 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 def test_errors_fail(self, monkeypatch: pytest.MonkeyPatch) -> None: """Errors should cause exit code 1.""" error_result = check_translations.TranslationCheckResult( name="devx", src_dir=Path("/tmp"), trans_file=Path("/tmp/t.json"), used_keys={"a"}, defined_keys={"a"}, errors=["Dead key: 'bar'"], ) monkeypatch.setattr(check_translations, "check_translation_set", lambda name, src, trans: error_result) runner = CliRunner() result = runner.invoke(check_translations.main, []) 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="devx", src_dir=Path("/tmp"), trans_file=Path("/tmp/t.json"), errors=["Missing key: 'foo'"], ) ok_result = check_translations.TranslationCheckResult( name="devx2", 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 == "devx" 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="devx", 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 def test_translations_flag(self, tmp_path: Path) -> None: """--translations flag should check a specific file.""" trans_file = tmp_path / "translations.json" all_langs = {"en": "Hello", "bg": "Здравей", "de": "Hallo", "ru": "Привет", "zh": "你好", "pl": "Cześć"} data = {"Hello": all_langs} for dk in check_translations.DYNAMIC_KEYS: data[dk] = all_langs trans_file.write_text(json.dumps(data)) (tmp_path / "mod.py").write_text('_("Hello")\n') runner = CliRunner() result = runner.invoke(check_translations.main, ["--translations", str(trans_file)]) assert result.exit_code == 0 def test_no_translations_file_skips(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """When no translations file is found, should pass with skip message.""" monkeypatch.chdir(tmp_path) runner = CliRunner() result = runner.invoke(check_translations.main, []) assert result.exit_code == 0 assert "No translations file found" 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 TestDevxI18n: """Test the devx.i18n module.""" def test_english_default(self) -> None: from devx.i18n import _ assert _("Running tests...") == "Running tests..." def test_format_kwargs(self) -> None: from devx.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 devx.i18n import _ assert _("Nonexistent key 12345") == "Nonexistent key 12345" def test_devx_lang_override(self, monkeypatch: pytest.MonkeyPatch) -> None: import importlib monkeypatch.setenv("DEVX_LANG", "de") import devx.i18n importlib.reload(devx.i18n) # "ERROR: CI_GITEA_TOKEN is not set." has a German translation result = devx.i18n._("ERROR: CI_GITEA_TOKEN is not set.") assert "FEHLER" in result # Restore monkeypatch.delenv("DEVX_LANG", raising=False) importlib.reload(devx.i18n) def test_polish_translation(self, monkeypatch: pytest.MonkeyPatch) -> None: import importlib monkeypatch.setenv("DEVX_LANG", "pl") import devx.i18n importlib.reload(devx.i18n) result = devx.i18n._("Running tests...") assert "Uruchamianie testów" in result monkeypatch.delenv("DEVX_LANG", raising=False) importlib.reload(devx.i18n) def test_unsupported_lang_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None: import importlib monkeypatch.setenv("DEVX_LANG", "xx") import devx.i18n importlib.reload(devx.i18n) result = devx.i18n._("Running tests...") assert result == "Running tests..." # Falls back to English monkeypatch.delenv("DEVX_LANG", raising=False) importlib.reload(devx.i18n) class TestTranslationsFile: """Test that the devx translations file is valid.""" def test_translations_exist(self) -> None: trans = Path("src/devx/translations.json") assert trans.exists() data = json.loads(trans.read_text()) assert len(data) > 0 def test_all_keys_have_english(self) -> None: trans = Path("src/devx/translations.json") data = json.loads(trans.read_text()) for key, langs in data.items(): assert "en" in langs, f"Key {key!r} missing English translation" class TestCollectKeys: def test_skips_i18n_py(self, tmp_path: Path) -> None: """i18n.py should be skipped when collecting keys.""" (tmp_path / "i18n.py").write_text('_("should_not_appear")\n') (tmp_path / "mod.py").write_text('_("should_appear")\n') keys = check_translations.collect_keys(tmp_path) assert "should_appear" in keys assert "should_not_appear" not in keys def test_non_default_dir_includes_dynamic_keys(self, tmp_path: Path) -> None: """Non-default source dirs should also include DYNAMIC_KEYS.""" (tmp_path / "mod.py").write_text('_("mykey")\n') keys = check_translations.collect_keys(tmp_path) assert "mykey" in keys # Dynamic keys should be present for all dirs assert "completed" in keys assert "pending" in keys def test_default_dir_includes_dynamic_keys(self, tmp_path: Path) -> None: """collect_keys includes DYNAMIC_KEYS even with an empty source dir.""" keys = check_translations.collect_keys(tmp_path) assert "completed" in keys assert "pending" in keys assert "in_progress" in keys class TestMainCleanPass: def test_passes_clean(self, monkeypatch: pytest.MonkeyPatch) -> None: """Should pass with exit code 0 and 'PASS:' message when no errors.""" ok_result = check_translations.TranslationCheckResult( name="devx", 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:" in result.output class TestI18nProjectTranslations: """Test devx.i18n._load_project_translations function.""" def test_loads_project_translations(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Project translations from DEVX_TRANSLATIONS_PATH are loaded.""" trans_file = tmp_path / "extra.json" trans_file.write_text(json.dumps({"Custom key": {"en": "Custom value"}})) monkeypatch.setenv("DEVX_TRANSLATIONS_PATH", str(trans_file)) import importlib import devx.i18n importlib.reload(devx.i18n) assert "Custom key" in devx.i18n.TRANSLATIONS # Restore monkeypatch.delenv("DEVX_TRANSLATIONS_PATH", raising=False) importlib.reload(devx.i18n) def test_nonexistent_path_returns_empty(self, monkeypatch: pytest.MonkeyPatch) -> None: """Nonexistent DEVX_TRANSLATIONS_PATH returns empty dict.""" monkeypatch.setenv("DEVX_TRANSLATIONS_PATH", "/nonexistent/path/file.json") import importlib import devx.i18n importlib.reload(devx.i18n) # Should still work with built-in translations assert devx.i18n._("Running tests...") == "Running tests..." monkeypatch.delenv("DEVX_TRANSLATIONS_PATH", raising=False) importlib.reload(devx.i18n) def test_invalid_json_returns_empty(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Invalid JSON in DEVX_TRANSLATIONS_PATH returns empty dict.""" trans_file = tmp_path / "bad.json" trans_file.write_text("{invalid json content") monkeypatch.setenv("DEVX_TRANSLATIONS_PATH", str(trans_file)) import importlib import devx.i18n importlib.reload(devx.i18n) # Should still work with built-in translations assert devx.i18n._("Running tests...") == "Running tests..." monkeypatch.delenv("DEVX_TRANSLATIONS_PATH", raising=False) importlib.reload(devx.i18n)