Files
devx/tests/unit/test_check_translations.py
T
emil 06e80516d4
Post-merge / detect-type (push) Successful in 19s
Post-merge / validate-commit-msg (push) Failing after 14s
Post-merge / configure-repo (push) Successful in 17s
Post-merge / release (push) Successful in 1m6s
Post-merge / vikunja (push) Successful in 20s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / badges (push) Successful in 1m12s
DEVX-61: optimise slow unit tests and handle missing tea binary in TeaCLI
2026-06-26 16:09:54 +00:00

394 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
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: REPO_TOKEN is not set." has a German translation
result = devx.i18n._("ERROR: REPO_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)