testsAndMisc-archive/python_pkg/word_frequency/tests/test_translator.py

714 lines
24 KiB
Python
Raw Normal View History

2025-12-28 15:55:43 +01:00
"""Tests for the offline translator module."""
from __future__ import annotations
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
import pytest
if TYPE_CHECKING:
from collections.abc import Generator
# Import the module
try:
from python_pkg.word_frequency import translator
from python_pkg.word_frequency.translator import (
TranslationResult,
download_languages,
format_translations,
get_available_packages,
get_installed_languages,
main,
read_file,
translate_word,
translate_words,
translate_words_batch,
)
except ImportError:
# Direct execution support
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from python_pkg.word_frequency import translator
from python_pkg.word_frequency.translator import (
TranslationResult,
download_languages,
format_translations,
get_available_packages,
get_installed_languages,
main,
read_file,
translate_word,
translate_words,
translate_words_batch,
)
# Helper context manager for mocking argostranslate
class ArgosAvailableMock:
"""Context manager to mock argostranslate being available and control its output.
Works whether argos is installed or not by patching sys.modules.
"""
2025-12-28 15:55:43 +01:00
def __init__(self, translate_returns: str | list[str] | Exception | None = None) -> None:
"""Initialize with return values for translate()."""
self.translate_returns = translate_returns
self.mock_translate_fn = MagicMock()
2025-12-28 15:55:43 +01:00
self.mock_translate_module = MagicMock()
self.mock_package_module = MagicMock()
self.mock_parent = MagicMock()
self.original_available = translator._argos_available
self._sys_modules_patcher: MagicMock | None = None
self._ensure_patcher: MagicMock | None = None
self._lang_patcher: MagicMock | None = None
2025-12-28 15:55:43 +01:00
def __enter__(self) -> MagicMock:
"""Set up the mocks."""
translator._argos_available = True
# Set up translate return value
if isinstance(self.translate_returns, Exception):
self.mock_translate_fn.side_effect = self.translate_returns
2025-12-28 15:55:43 +01:00
elif isinstance(self.translate_returns, list):
self.mock_translate_fn.side_effect = self.translate_returns
2025-12-28 15:55:43 +01:00
elif self.translate_returns is not None:
self.mock_translate_fn.return_value = self.translate_returns
2025-12-28 15:55:43 +01:00
# Wire up the mock modules
self.mock_translate_module.translate = self.mock_translate_fn
self.mock_translate_module.get_installed_languages = MagicMock(return_value=[])
self.mock_package_module.update_package_index = MagicMock()
self.mock_package_module.get_available_packages = MagicMock(return_value=[])
2025-12-28 15:55:43 +01:00
self.mock_parent.translate = self.mock_translate_module
self.mock_parent.package = self.mock_package_module
# Patch sys.modules to inject our mock (works even if argos not installed)
self._sys_modules_patcher = patch.dict(
"sys.modules",
{
"argostranslate": self.mock_parent,
"argostranslate.translate": self.mock_translate_module,
"argostranslate.package": self.mock_package_module,
},
)
# Patch _ensure_argos_installed and _ensure_language_pair to no-op
self._ensure_patcher = patch.object(
translator, "_ensure_argos_installed", lambda: None
)
self._lang_patcher = patch.object(
translator, "_ensure_language_pair", lambda f, t: None
)
2025-12-28 15:55:43 +01:00
self._sys_modules_patcher.start()
self._ensure_patcher.start()
self._lang_patcher.start()
return self.mock_translate_fn
2025-12-28 15:55:43 +01:00
def __exit__(self, *args: object) -> None:
"""Restore original state."""
if self._lang_patcher:
self._lang_patcher.stop()
if self._ensure_patcher:
self._ensure_patcher.stop()
if self._sys_modules_patcher:
self._sys_modules_patcher.stop()
2025-12-28 15:55:43 +01:00
translator._argos_available = self.original_available
# Fixtures
@pytest.fixture
def mock_argos_unavailable() -> Generator[None, None, None]:
"""Mock argostranslate being unavailable (for legacy tests)."""
2025-12-28 15:55:43 +01:00
original_value = translator._argos_available
translator._argos_available = False
yield
translator._argos_available = original_value
@pytest.fixture
def temp_words_file(tmp_path: Path) -> Path:
"""Create a temporary file with words."""
words_file = tmp_path / "words.txt"
words_file.write_text("hello\nworld\ngoodbye\n", encoding="utf-8")
return words_file
# TranslationResult tests
class TestTranslationResult:
"""Tests for TranslationResult namedtuple."""
def test_successful_result(self) -> None:
"""Test creating a successful translation result."""
result = TranslationResult(
source_word="hello",
translated_word="hola",
source_lang="en",
target_lang="es",
success=True,
)
assert result.source_word == "hello"
assert result.translated_word == "hola"
assert result.source_lang == "en"
assert result.target_lang == "es"
assert result.success is True
assert result.error is None
def test_failed_result(self) -> None:
"""Test creating a failed translation result."""
result = TranslationResult(
source_word="xyz",
translated_word="",
source_lang="en",
target_lang="xx",
success=False,
error="Language not supported",
)
assert result.success is False
assert result.error == "Language not supported"
def test_result_is_tuple(self) -> None:
"""Test that TranslationResult is a namedtuple."""
result = TranslationResult("a", "b", "en", "es", True)
assert isinstance(result, tuple)
assert len(result) == 6
# translate_word tests
class TestTranslateWord:
"""Tests for translate_word function - offline-first behavior."""
def test_translate_word_argos_unavailable_raises(self) -> None:
"""Test that translation raises ImportError when argos is unavailable."""
# Mock _ensure_argos_installed to raise ImportError
with patch.object(
translator,
"_ensure_argos_installed",
side_effect=ImportError("argostranslate not available"),
):
with pytest.raises(ImportError, match="argostranslate not available"):
translate_word("hello", "en", "es", use_cache=False)
2025-12-28 15:55:43 +01:00
def test_translate_word_success(self) -> None:
"""Test successful word translation."""
with ArgosAvailableMock("hola"):
result = translate_word("hello", "en", "es", use_cache=False)
2025-12-28 15:55:43 +01:00
assert result.source_word == "hello"
assert result.translated_word == "hola"
assert result.success is True
def test_translate_word_argos_exception_returns_error(self) -> None:
"""Test that argos exception returns failed result with error."""
# Mock argos being available but translate raising an exception
with ArgosAvailableMock(RuntimeError("Translation failed")):
result = translate_word("hello", "en", "es", use_cache=False)
assert result.success is False
assert "Translation failed" in str(result.error)
2025-12-28 15:55:43 +01:00
# translate_words tests
class TestTranslateWords:
"""Tests for translate_words function."""
def test_translate_empty_list(self) -> None:
"""Test translating empty list."""
# Empty list returns empty result without calling translation
2025-12-28 15:55:43 +01:00
results = translate_words([], "en", "es")
assert results == []
def test_translate_multiple_words(self) -> None:
"""Test translating multiple words."""
with ArgosAvailableMock(["hola", "mundo"]) as mock:
mock.side_effect = ["hola", "mundo"]
results = translate_words(["hello", "world"], "en", "es", use_cache=False)
2025-12-28 15:55:43 +01:00
assert len(results) == 2
assert results[0].translated_word == "hola"
assert results[1].translated_word == "mundo"
def test_translate_words_argos_unavailable_raises(self) -> None:
"""Test that translating words raises ImportError when argos unavailable."""
with patch.object(
translator,
"_ensure_argos_installed",
side_effect=ImportError("argostranslate not available"),
):
with pytest.raises(ImportError, match="argostranslate not available"):
translate_words(["hello", "world"], "en", "es", use_cache=False)
2025-12-28 15:55:43 +01:00
# translate_words_batch tests
class TestTranslateWordsBatch:
"""Tests for translate_words_batch function - offline-first."""
2025-12-28 15:55:43 +01:00
def test_batch_empty_list(self) -> None:
"""Test batch translation of empty list."""
# Empty list doesn't require argos
with patch.object(translator, "_ensure_argos_installed", lambda: None):
results = translate_words_batch([], "en", "es")
2025-12-28 15:55:43 +01:00
assert results == []
def test_batch_small_list(self) -> None:
"""Test batch translation of small list (uses batch mode anyway)."""
with ArgosAvailableMock("uno\ndos\ntres") as mock:
results = translate_words_batch(
["one", "two", "three"], "en", "es", use_cache=False
)
2025-12-28 15:55:43 +01:00
assert len(results) == 3
# Batch translation
assert mock.call_count == 1
2025-12-28 15:55:43 +01:00
def test_batch_large_list_success(self) -> None:
"""Test batch translation of large list."""
words = ["one", "two", "three", "four", "five"]
with ArgosAvailableMock("uno\ndos\ntres\ncuatro\ncinco") as mock:
results = translate_words_batch(words, "en", "es", use_cache=False)
2025-12-28 15:55:43 +01:00
assert len(results) == 5
# Batch translation called once
mock.assert_called_once()
2025-12-28 15:55:43 +01:00
assert results[0].translated_word == "uno"
assert results[4].translated_word == "cinco"
def test_batch_fallback_on_mismatch(self) -> None:
"""Test batch translation falls back to individual when result count mismatches."""
2025-12-28 15:55:43 +01:00
words = ["one", "two", "three", "four"]
# First call (batch) returns wrong count, subsequent calls are individual
with ArgosAvailableMock(
["wrong", "uno", "dos", "tres", "cuatro"]
2025-12-28 15:55:43 +01:00
) as mock:
results = translate_words_batch(words, "en", "es", use_cache=False)
2025-12-28 15:55:43 +01:00
assert len(results) == 4
# Fallback to individual argos translation
assert mock.call_count == 5
2025-12-28 15:55:43 +01:00
def test_batch_fallback_on_exception(self) -> None:
"""Test batch translation raises on exception (no fallback to online)."""
2025-12-28 15:55:43 +01:00
words = ["one", "two", "three", "four"]
# Create mock that raises
mock_translate = MagicMock(side_effect=RuntimeError("Batch failed"))
2025-12-28 15:55:43 +01:00
mock_translate_module = MagicMock()
mock_translate_module.translate = mock_translate
2025-12-28 15:55:43 +01:00
mock_package_module = MagicMock()
mock_parent = MagicMock()
mock_parent.translate = mock_translate_module
mock_parent.package = mock_package_module
original = translator._argos_available
translator._argos_available = True
with (
patch.dict(
"sys.modules",
{
"argostranslate": mock_parent,
"argostranslate.translate": mock_translate_module,
"argostranslate.package": mock_package_module,
},
),
patch.object(translator, "_ensure_argos_installed", lambda: None),
patch.object(translator, "_ensure_language_pair", lambda f, t: None),
pytest.raises(RuntimeError, match="Translation failed"),
2025-12-28 15:55:43 +01:00
):
translate_words_batch(words, "en", "es", use_cache=False)
2025-12-28 15:55:43 +01:00
translator._argos_available = original
def test_batch_argos_unavailable_raises(self) -> None:
"""Test that batch translation raises ImportError when argos unavailable."""
with patch.object(
translator,
"_ensure_argos_installed",
side_effect=ImportError("argostranslate not available"),
):
with pytest.raises(ImportError, match="argostranslate not available"):
translate_words_batch(["hello", "world"], "en", "es", use_cache=False)
2025-12-28 15:55:43 +01:00
# format_translations tests
class TestFormatTranslations:
"""Tests for format_translations function."""
def test_format_empty(self) -> None:
"""Test formatting empty results."""
output = format_translations([])
assert output == "No translations."
def test_format_single_translation(self) -> None:
"""Test formatting single translation."""
results = [
TranslationResult("hello", "hola", "en", "es", True),
]
output = format_translations(results)
assert "en -> es" in output
assert "hello" in output
assert "hola" in output
def test_format_multiple_translations(self) -> None:
"""Test formatting multiple translations."""
results = [
TranslationResult("hello", "hola", "en", "es", True),
TranslationResult("world", "mundo", "en", "es", True),
]
output = format_translations(results)
assert "hello" in output
assert "hola" in output
assert "world" in output
assert "mundo" in output
def test_format_with_errors(self) -> None:
"""Test formatting with failed translations."""
results = [
TranslationResult("hello", "hola", "en", "es", True),
TranslationResult("xyz", "", "en", "es", False, "Unknown word"),
]
output = format_translations(results, show_errors=True)
assert "hello" in output
assert "Error: Unknown word" in output
def test_format_hide_errors(self) -> None:
"""Test formatting with errors hidden."""
results = [
TranslationResult("hello", "hola", "en", "es", True),
TranslationResult("xyz", "", "en", "es", False, "Unknown word"),
]
output = format_translations(results, show_errors=False)
assert "hello" in output
assert "Unknown word" not in output
# get_installed_languages tests
class TestGetInstalledLanguages:
"""Tests for get_installed_languages function."""
def test_argos_unavailable(self, mock_argos_unavailable: None) -> None:
"""Test when argos is unavailable."""
result = get_installed_languages()
assert result == []
def test_returns_languages(self) -> None:
"""Test returning installed languages."""
mock_lang1 = MagicMock()
mock_lang1.code = "en"
mock_lang1.name = "English"
mock_lang2 = MagicMock()
mock_lang2.code = "es"
mock_lang2.name = "Spanish"
# We need to mock the translate module's get_installed_languages
mock_translate_module = MagicMock()
mock_translate_module.get_installed_languages.return_value = [
mock_lang1, mock_lang2
]
mock_package_module = MagicMock()
mock_parent = MagicMock()
mock_parent.translate = mock_translate_module
mock_parent.package = mock_package_module
original = translator._argos_available
translator._argos_available = True
with patch.dict(
"sys.modules",
{
"argostranslate": mock_parent,
"argostranslate.translate": mock_translate_module,
"argostranslate.package": mock_package_module,
},
):
2025-12-28 15:55:43 +01:00
result = get_installed_languages()
translator._argos_available = original
2025-12-28 15:55:43 +01:00
assert ("en", "English") in result
assert ("es", "Spanish") in result
# get_available_packages tests
class TestGetAvailablePackages:
"""Tests for get_available_packages function."""
def test_argos_unavailable(self, mock_argos_unavailable: None) -> None:
"""Test when argos is unavailable."""
result = get_available_packages()
assert result == []
# download_languages tests
class TestDownloadLanguages:
"""Tests for download_languages function."""
def test_argos_unavailable(self, mock_argos_unavailable: None) -> None:
"""Test when argos is unavailable."""
result = download_languages(["en", "es"])
assert result == {}
# read_file tests
class TestReadFile:
"""Tests for read_file function."""
def test_read_file(self, tmp_path: Path) -> None:
"""Test reading a file."""
test_file = tmp_path / "test.txt"
test_file.write_text("hello\nworld", encoding="utf-8")
content = read_file(test_file)
assert content == "hello\nworld"
def test_read_file_not_found(self, tmp_path: Path) -> None:
"""Test reading non-existent file."""
with pytest.raises(FileNotFoundError):
read_file(tmp_path / "nonexistent.txt")
# main function tests
class TestMain:
"""Tests for main CLI function."""
def test_argos_unavailable_error(self, mock_argos_unavailable: None) -> None:
"""Test error when argos not installed."""
result = main(["--text", "hello", "--from", "en", "--to", "es"])
assert result == 1
def test_list_languages_empty(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""Test listing languages when none installed."""
mock_translate_module = MagicMock()
mock_translate_module.get_installed_languages.return_value = []
mock_package_module = MagicMock()
mock_parent = MagicMock()
mock_parent.translate = mock_translate_module
mock_parent.package = mock_package_module
original = translator._argos_available
translator._argos_available = True
with patch.dict(
"sys.modules",
{
"argostranslate": mock_parent,
"argostranslate.translate": mock_translate_module,
"argostranslate.package": mock_package_module,
},
):
2025-12-28 15:55:43 +01:00
result = main(["--list-languages"])
translator._argos_available = original
2025-12-28 15:55:43 +01:00
assert result == 0
captured = capsys.readouterr()
assert "No languages installed" in captured.out
def test_list_languages_with_results(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""Test listing installed languages."""
mock_lang = MagicMock()
mock_lang.code = "en"
mock_lang.name = "English"
mock_translate_module = MagicMock()
mock_translate_module.get_installed_languages.return_value = [mock_lang]
mock_package_module = MagicMock()
mock_parent = MagicMock()
mock_parent.translate = mock_translate_module
mock_parent.package = mock_package_module
original = translator._argos_available
translator._argos_available = True
with patch.dict(
"sys.modules",
{
"argostranslate": mock_parent,
"argostranslate.translate": mock_translate_module,
"argostranslate.package": mock_package_module,
},
):
2025-12-28 15:55:43 +01:00
result = main(["--list-languages"])
translator._argos_available = original
2025-12-28 15:55:43 +01:00
assert result == 0
captured = capsys.readouterr()
assert "en" in captured.out
assert "English" in captured.out
def test_translate_single_text(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""Test translating single text."""
with ArgosAvailableMock("hola"):
result = main(["--text", "hello", "--from", "en", "--to", "es"])
assert result == 0
captured = capsys.readouterr()
assert "hello" in captured.out
assert "hola" in captured.out
def test_translate_multiple_words(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""Test translating multiple words."""
with ArgosAvailableMock(["hola", "mundo"]):
result = main(["--words", "hello", "world", "--from", "en", "--to", "es"])
assert result == 0
captured = capsys.readouterr()
assert "hello" in captured.out
assert "world" in captured.out
def test_translate_from_file(
self,
temp_words_file: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Test translating words from file."""
with ArgosAvailableMock(["hola", "mundo", "adios"]):
result = main(
["--words-file", str(temp_words_file), "--from", "en", "--to", "es"]
)
assert result == 0
captured = capsys.readouterr()
assert "hello" in captured.out
assert "world" in captured.out
assert "goodbye" in captured.out
def test_translate_file_not_found(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""Test error when words file not found."""
with ArgosAvailableMock():
result = main(
["--words-file", "/nonexistent/file.txt", "--from", "en", "--to", "es"]
)
assert result == 1
captured = capsys.readouterr()
assert "File not found" in captured.err
def test_translate_output_to_file(
self,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Test outputting translations to file."""
output_file = tmp_path / "output.txt"
with ArgosAvailableMock("hola"):
result = main(
[
"--text",
"hello",
"--from",
"en",
"--to",
"es",
"--output",
str(output_file),
]
)
assert result == 0
assert output_file.exists()
content = output_file.read_text(encoding="utf-8")
assert "hello" in content
assert "hola" in content
def test_no_input_shows_help(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""Test that no input shows help."""
with ArgosAvailableMock():
result = main([])
assert result == 1
def test_translation_failure_returns_error(self) -> None:
"""Test that translation failure returns error code when argos unavailable."""
with patch.object(
translator,
"_ensure_argos_installed",
side_effect=ImportError("argostranslate not available"),
):
result = main(["--text", "hello", "--from", "en", "--to", "es"])
2025-12-28 15:55:43 +01:00
assert result == 1
# Integration-style tests (still mocked but testing more flow)
class TestIntegration:
"""Integration-style tests for translator."""
def test_full_translation_flow(self) -> None:
"""Test complete translation flow."""
with ArgosAvailableMock(["uno", "dos", "tres"]) as mock:
mock.side_effect = ["uno", "dos", "tres"]
2025-12-28 15:55:43 +01:00
words = ["one", "two", "three"]
results = translate_words(words, "en", "es", use_cache=False)
2025-12-28 15:55:43 +01:00
assert all(r.success for r in results)
assert [r.translated_word for r in results] == ["uno", "dos", "tres"]
output = format_translations(results)
assert "en -> es" in output
assert "one" in output
assert "uno" in output
def test_mixed_success_failure(self) -> None:
"""Test handling when argos raises exception for some translations."""
# Simulate argos translating first word, then failing, then succeeding
with ArgosAvailableMock() as mock:
mock.side_effect = ["hola", RuntimeError("Unknown"), "mundo"]
results = translate_words(
["hello", "xyz", "world"], "en", "es", use_cache=False
)
2025-12-28 15:55:43 +01:00
# First and third succeed, second fails
assert results[0].success is True
assert results[1].success is False
assert results[2].success is True
2025-12-28 15:55:43 +01:00
output = format_translations(results)
assert "Error" in output