testsAndMisc/python_pkg/word_frequency/tests/test_translator.py

620 lines
20 KiB
Python

"""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."""
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_module = MagicMock()
self.mock_package_module = MagicMock()
self.mock_parent = MagicMock()
self.original_available = translator._argos_available
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_module.translate.side_effect = self.translate_returns
elif isinstance(self.translate_returns, list):
self.mock_translate_module.translate.side_effect = self.translate_returns
elif self.translate_returns is not None:
self.mock_translate_module.translate.return_value = self.translate_returns
# Link parent module to submodules (critical for Python imports)
self.mock_parent.translate = self.mock_translate_module
self.mock_parent.package = self.mock_package_module
# Patch sys.modules
self.patchers = [
patch.dict(
"sys.modules",
{
"argostranslate": self.mock_parent,
"argostranslate.translate": self.mock_translate_module,
"argostranslate.package": self.mock_package_module,
},
),
]
for p in self.patchers:
p.start()
return self.mock_translate_module
def __exit__(self, *args: object) -> None:
"""Restore original state."""
for p in self.patchers:
p.stop()
translator._argos_available = self.original_available
# Fixtures
@pytest.fixture
def mock_argos_unavailable() -> Generator[None, None, None]:
"""Mock argostranslate being unavailable."""
original_value = translator._argos_available
translator._argos_available = False
yield
translator._argos_available = original_value
@pytest.fixture
def mock_all_translators_unavailable() -> Generator[None, None, None]:
"""Mock both argostranslate and deep-translator being unavailable."""
original_argos = translator._argos_available
original_deep = translator._deep_translator_available
translator._argos_available = False
translator._deep_translator_available = False
yield
translator._argos_available = original_argos
translator._deep_translator_available = original_deep
@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."""
def test_translate_word_all_backends_unavailable(
self, mock_all_translators_unavailable: None
) -> None:
"""Test translation when no backends are available."""
result = translate_word("hello", "en", "es")
assert result.success is False
assert "No translation backend" in str(result.error)
def test_translate_word_argos_unavailable_uses_deep_translator(
self, mock_argos_unavailable: None
) -> None:
"""Test that deep-translator is used when argos is unavailable."""
# deep-translator should work as fallback (it's installed)
result = translate_word("hello", "en", "es")
# This may succeed if deep-translator is installed
# Just verify we get a result without crashing
assert isinstance(result, TranslationResult)
def test_translate_word_success(self) -> None:
"""Test successful word translation."""
with ArgosAvailableMock("hola"):
result = translate_word("hello", "en", "es")
assert result.source_word == "hello"
assert result.translated_word == "hola"
assert result.success is True
def test_translate_word_argos_exception_falls_back(
self, mock_argos_unavailable: None
) -> None:
"""Test that argos exception falls back to deep-translator."""
# With argos unavailable, deep-translator should be used
result = translate_word("hello", "en", "es")
# Just verify it doesn't crash - may succeed or fail depending on network
assert isinstance(result, TranslationResult)
# translate_words tests
class TestTranslateWords:
"""Tests for translate_words function."""
def test_translate_empty_list(self) -> None:
"""Test translating empty list."""
results = translate_words([], "en", "es")
assert results == []
def test_translate_multiple_words(self) -> None:
"""Test translating multiple words."""
with ArgosAvailableMock(["hola", "mundo"]):
results = translate_words(["hello", "world"], "en", "es")
assert len(results) == 2
assert results[0].translated_word == "hola"
assert results[1].translated_word == "mundo"
# translate_words_batch tests
class TestTranslateWordsBatch:
"""Tests for translate_words_batch function."""
def test_batch_empty_list(self) -> None:
"""Test batch translation of empty list."""
results = translate_words_batch([], "en", "es")
assert results == []
def test_batch_small_list(self) -> None:
"""Test batch translation of small list (3 or fewer)."""
with ArgosAvailableMock(["uno", "dos", "tres"]) as mock:
results = translate_words_batch(["one", "two", "three"], "en", "es")
assert len(results) == 3
# Small lists use individual translation
assert mock.translate.call_count == 3
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")
assert len(results) == 5
# Batch translation called once
mock.translate.assert_called_once()
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 when result count mismatches."""
words = ["one", "two", "three", "four"]
# First call (batch) returns wrong count, subsequent calls are individual
with ArgosAvailableMock(
["wrong\ncount", "uno", "dos", "tres", "cuatro"]
) as mock:
results = translate_words_batch(words, "en", "es")
assert len(results) == 4
# Fallback to individual
assert mock.translate.call_count == 5
def test_batch_fallback_on_exception(self) -> None:
"""Test batch translation falls back on exception."""
words = ["one", "two", "three", "four"]
# Create mock that raises first then succeeds
original = translator._argos_available
translator._argos_available = True
mock_translate_module = MagicMock()
mock_translate_module.translate.side_effect = [
RuntimeError("Batch failed"),
"uno",
"dos",
"tres",
"cuatro",
]
mock_package_module = MagicMock()
mock_parent = MagicMock()
mock_parent.translate = mock_translate_module
mock_parent.package = mock_package_module
with patch.dict(
"sys.modules",
{
"argostranslate": mock_parent,
"argostranslate.translate": mock_translate_module,
"argostranslate.package": mock_package_module,
},
):
results = translate_words_batch(words, "en", "es")
translator._argos_available = original
assert len(results) == 4
# 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"
with ArgosAvailableMock() as mock:
mock.get_installed_languages.return_value = [mock_lang1, mock_lang2]
result = get_installed_languages()
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."""
with ArgosAvailableMock() as mock:
mock.get_installed_languages.return_value = []
result = main(["--list-languages"])
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"
with ArgosAvailableMock() as mock:
mock.get_installed_languages.return_value = [mock_lang]
result = main(["--list-languages"])
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, mock_all_translators_unavailable: None
) -> None:
"""Test that translation failure returns error code when no backends."""
result = main(["--text", "hello", "--from", "en", "--to", "es"])
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"]):
words = ["one", "two", "three"]
results = translate_words(words, "en", "es")
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, mock_all_translators_unavailable: None
) -> None:
"""Test handling when no translation backends are available."""
results = translate_words(["hello", "xyz", "world"], "en", "es")
# All should fail when no backends available
assert all(not r.success for r in results)
output = format_translations(results)
assert "Error" in output