mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 19:43:14 +02:00
620 lines
20 KiB
Python
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
|