Public Access
Post-merge / detect-type (push) Successful in 10s
Post-merge / validate-commit-msg (push) Successful in 8s
Post-merge / vikunja (push) Successful in 13s
Post-merge / configure-repo (push) Successful in 11s
Post-merge / sync-wiki (push) Successful in 21s
Post-merge / release (push) Successful in 25s
Build Images / detect-type (push) Successful in 38s
Post-merge / badges (push) Successful in 34s
Post-merge / publish (push) Successful in 33s
Build Images / build-and-push (push) Successful in 3m20s
Build Images / cleanup (push) Successful in 1m57s
402 lines
15 KiB
Python
402 lines
15 KiB
Python
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)
|