""" Tests for the OllamaTranslationProvider. """ import pytest from unittest.mock import patch, MagicMock from requests.exceptions import Timeout, ConnectionError as RequestsConnectionError from services.providers.ollama_provider import ( OllamaTranslationProvider, OllamaProviderError, get_ollama_provider, register_ollama_provider, _build_system_prompt, _get_language_name, OLLAMA_UNAVAILABLE, OLLAMA_MODEL_NOT_FOUND, OLLAMA_TIMEOUT, OLLAMA_GENERATION_ERROR, OLLAMA_CONTEXT_TOO_LONG, ) from services.providers.schemas import TranslationRequest, TranslationResponse class TestOllamaProviderError: """Tests for OllamaProviderError exception.""" def test_error_creation(self): """Test error creation with all fields.""" error = OllamaProviderError( code=OLLAMA_UNAVAILABLE, message="Ollama unavailable", details={"provider": "ollama"}, ) assert error.code == OLLAMA_UNAVAILABLE assert error.message == "Ollama unavailable" assert error.details == {"provider": "ollama"} def test_error_to_dict(self): """Test error serialization.""" error = OllamaProviderError( code=OLLAMA_MODEL_NOT_FOUND, message="Model not found", details={"model": "llama3"}, ) result = error.to_dict() assert result["error"] == OLLAMA_MODEL_NOT_FOUND assert result["message"] == "Model not found" assert result["details"]["model"] == "llama3" def test_error_to_dict_no_details(self): """Test error serialization without details.""" error = OllamaProviderError( code=OLLAMA_GENERATION_ERROR, message="Generation error", ) result = error.to_dict() assert result["error"] == OLLAMA_GENERATION_ERROR assert result["message"] == "Generation error" assert "details" not in result class TestHelperFunctions: """Tests for helper functions.""" def test_get_language_name_common(self): """Test language name lookup for common languages.""" assert _get_language_name("en") == "English" assert _get_language_name("fr") == "French" assert _get_language_name("es") == "Spanish" assert _get_language_name("de") == "German" assert _get_language_name("zh") == "Chinese" assert _get_language_name("ja") == "Japanese" def test_get_language_name_with_variant(self): """Test language name lookup with variant codes.""" assert _get_language_name("en-US") == "English" assert _get_language_name("pt-BR") == "Portuguese" def test_get_language_name_unknown(self): """Test language name lookup for unknown codes.""" assert _get_language_name("xx") == "xx" def test_build_system_prompt_default(self): """Test default system prompt generation.""" prompt = _build_system_prompt("English", "French") assert "English" in prompt assert "French" in prompt assert "translator" in prompt.lower() def test_build_system_prompt_custom(self): """Test custom system prompt.""" custom = "Translate this text formally for business context." prompt = _build_system_prompt("English", "French", custom) assert prompt == custom class TestOllamaTranslationProvider: """Tests for OllamaTranslationProvider.""" @pytest.fixture def provider(self): """Create an Ollama provider instance.""" return OllamaTranslationProvider( base_url="http://localhost:11434", model="llama3", timeout=120, max_retries=0, ) @pytest.fixture def provider_with_retries(self): """Create an Ollama provider with retries.""" return OllamaTranslationProvider( base_url="http://localhost:11434", model="llama3", timeout=120, max_retries=2, retry_delay=0.01, ) def test_init(self, provider): """Test provider initialization.""" assert provider._base_url == "http://localhost:11434" assert provider._model == "llama3" assert provider.timeout == 120 assert provider._provider_name == "ollama" def test_get_name(self, provider): """Test provider name.""" assert provider.get_name() == "ollama" def test_translate_text_empty(self, provider): """Test translating empty text.""" request = TranslationRequest(text="", target_language="fr") response = provider.translate_text(request) assert response.translated_text == "" assert response.provider_name == "ollama" assert response.from_cache is False def test_translate_text_whitespace(self, provider): """Test translating whitespace-only text.""" request = TranslationRequest(text=" ", target_language="fr") response = provider.translate_text(request) assert response.translated_text == " " @patch.object(OllamaTranslationProvider, "_fetch_available_models") @patch.object(OllamaTranslationProvider, "_make_api_request") def test_translate_text_success(self, mock_request, mock_models, provider): """Test successful translation.""" mock_models.return_value = ["llama3", "mistral"] mock_request.return_value = "Bonjour" request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.translated_text == "Bonjour" assert response.provider_name == "ollama" assert response.from_cache is False @patch.object(OllamaTranslationProvider, "_fetch_available_models") @patch.object(OllamaTranslationProvider, "_make_api_request") def test_translate_text_with_custom_prompt( self, mock_request, mock_models, provider ): """Test translation with custom system prompt.""" mock_models.return_value = ["llama3"] mock_request.return_value = "Bonjour (formal)" request = TranslationRequest( text="Hello", target_language="fr", metadata={"custom_prompt": "Translate formally for business"}, ) response = provider.translate_text(request) assert response.translated_text == "Bonjour (formal)" mock_request.assert_called_once() call_args = mock_request.call_args assert "Translate formally for business" in call_args[0][1] def test_translate_batch_empty(self, provider): """Test batch translation with empty list.""" responses = provider.translate_batch([]) assert responses == [] @patch.object(OllamaTranslationProvider, "translate_text") def test_translate_batch(self, mock_translate, provider): """Test batch translation.""" mock_translate.side_effect = [ TranslationResponse(translated_text="Bonjour", provider_name="ollama"), TranslationResponse(translated_text="Monde", provider_name="ollama"), ] requests = [ TranslationRequest(text="Hello", target_language="fr"), TranslationRequest(text="World", target_language="fr"), ] responses = provider.translate_batch(requests) assert len(responses) == 2 assert responses[0].translated_text == "Bonjour" assert responses[1].translated_text == "Monde" class TestOllamaErrorCodes: """Tests for Ollama error code handling.""" @pytest.fixture def provider(self): """Create an Ollama provider with no retries for error testing.""" return OllamaTranslationProvider( base_url="http://localhost:11434", model="llama3", timeout=120, max_retries=0, ) def test_context_too_long_error(self, provider): """Test context too long error.""" long_text = "x" * 130000 request = TranslationRequest(text=long_text, target_language="fr") response = provider.translate_text(request) assert response.error_code == OLLAMA_CONTEXT_TOO_LONG assert response.error is not None @patch.object(OllamaTranslationProvider, "_fetch_available_models") def test_model_not_found_error(self, mock_models, provider): """Test model not found error.""" mock_models.return_value = ["mistral", "qwen2"] request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.error_code == OLLAMA_MODEL_NOT_FOUND assert "llama3" in response.error @patch.object(OllamaTranslationProvider, "_check_model_available") @patch("services.providers.ollama_provider.requests") def test_unavailable_error(self, mock_requests, mock_model_available, provider): """Test Ollama unavailable error.""" mock_model_available.return_value = True mock_requests.post.side_effect = RequestsConnectionError("Connection refused") request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.error_code == OLLAMA_UNAVAILABLE assert "indisponible" in response.error.lower() class TestOllamaHealthCheck: """Tests for Ollama health check functionality.""" @pytest.fixture def provider(self): """Create an Ollama provider instance.""" return OllamaTranslationProvider( base_url="http://localhost:11434", model="llama3", ) @patch("services.providers.ollama_provider.requests") def test_health_check_available(self, mock_requests, provider): """Test health check when Ollama is available.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "models": [{"name": "llama3"}, {"name": "mistral"}] } mock_requests.get.return_value = mock_response status = provider.health_check() assert status.name == "ollama" assert status.available is True assert status.latency_ms is not None assert status.model == "llama3" assert status.model_available is True @patch("services.providers.ollama_provider.requests") def test_health_check_model_not_pulled(self, mock_requests, provider): """Test health check when model is not pulled.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"models": [{"name": "mistral"}]} mock_requests.get.return_value = mock_response status = provider.health_check() assert status.available is False assert "llama3" in status.error assert status.model == "llama3" assert status.model_available is False @patch("services.providers.ollama_provider.requests") def test_health_check_unavailable(self, mock_requests, provider): """Test health check when Ollama is unavailable.""" mock_requests.get.side_effect = RequestsConnectionError("Connection refused") status = provider.health_check() assert status.available is False @patch("services.providers.ollama_provider.requests") def test_health_check_caching(self, mock_requests, provider): """Test that health check results are cached (no API call when cache valid).""" import time from services.providers.schemas import ProviderHealthStatus current_time = time.time() cached_status = ProviderHealthStatus( name="ollama", available=True, latency_ms=50.0, error=None, last_check="2024-01-15T10:00:00Z", ) provider._health_cache["health_check"] = { "value": cached_status, "timestamp": current_time, } status = provider.health_check() assert status.available is True mock_requests.get.assert_not_called() class TestOllamaProviderRetry: """Tests for Ollama provider retry logic.""" @pytest.fixture def provider(self): """Create an Ollama provider with retry enabled.""" return OllamaTranslationProvider( base_url="http://localhost:11434", model="llama3", max_retries=2, retry_delay=0.01, ) @patch.object(OllamaTranslationProvider, "_fetch_available_models") @patch.object(OllamaTranslationProvider, "_make_api_request") def test_retry_on_timeout(self, mock_request, mock_models, provider): """Test that timeout errors trigger retry.""" mock_models.return_value = ["llama3"] mock_request.side_effect = [ OllamaProviderError(OLLAMA_TIMEOUT, "Timeout"), "Bonjour", ] request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.translated_text == "Bonjour" assert mock_request.call_count == 2 @patch.object(OllamaTranslationProvider, "_fetch_available_models") @patch.object(OllamaTranslationProvider, "_make_api_request") def test_no_retry_on_model_not_found(self, mock_request, mock_models, provider): """Test that model not found errors do not trigger retry.""" mock_models.return_value = ["llama3"] mock_request.side_effect = OllamaProviderError( OLLAMA_MODEL_NOT_FOUND, "Model not found" ) request = TranslationRequest(text="Hello", target_language="fr") provider.translate_text(request) assert mock_request.call_count == 1 @patch.object(OllamaTranslationProvider, "_fetch_available_models") @patch.object(OllamaTranslationProvider, "_make_api_request") def test_timeout_returns_ollama_timeout_error(self, mock_request, mock_models): """Test that timeout without retry returns OLLAMA_TIMEOUT in response.""" provider = OllamaTranslationProvider( base_url="http://localhost:11434", model="llama3", timeout=120, max_retries=0, ) mock_models.return_value = ["llama3"] mock_request.side_effect = OllamaProviderError( OLLAMA_TIMEOUT, "Délai d'attente Ollama dépassé. Réessayez avec un texte plus court.", ) request = TranslationRequest(text="Hello", target_language="fr") response = provider.translate_text(request) assert response.error_code == OLLAMA_TIMEOUT assert response.error is not None assert "Délai" in response.error or "timeout" in response.error.lower() class TestOllamaProviderSingleton: """Tests for Ollama provider singleton functions.""" def test_get_ollama_provider(self): """Test get_ollama_provider creates instance with config.""" import services.providers.ollama_provider as ollama_module ollama_module._provider_instance = None with patch( "services.providers.config.ProvidersConfig" ) as mock_config: mock_config.OLLAMA_BASE_URL = "http://localhost:11434" mock_config.OLLAMA_MODEL = "llama3" mock_config.OLLAMA_TIMEOUT = 120 mock_config.OLLAMA_MAX_RETRIES = 2 mock_config.OLLAMA_RETRY_DELAY = 2.0 provider = ollama_module.get_ollama_provider() assert provider is not None assert provider._model == "llama3" ollama_module._provider_instance = None class TestOllamaRegistryIntegration: """Tests for Ollama provider registry integration.""" def test_register_ollama_provider(self): """Test provider registration.""" from services.providers.registry import registry registry.unregister("ollama") with patch( "services.providers.ollama_provider.get_ollama_provider" ) as mock_get: mock_provider = MagicMock() mock_get.return_value = mock_provider result = register_ollama_provider() assert result == mock_provider assert "ollama" in registry registry.unregister("ollama") class TestOllamaModelCheck: """Tests for Ollama model availability checking.""" @pytest.fixture def provider(self): """Create an Ollama provider instance.""" return OllamaTranslationProvider( base_url="http://localhost:11434", model="llama3", ) @patch("services.providers.ollama_provider.requests") def test_fetch_available_models(self, mock_requests, provider): """Test fetching available models.""" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = { "models": [ {"name": "llama3:latest"}, {"name": "mistral:latest"}, ] } mock_requests.get.return_value = mock_response models = provider._fetch_available_models() assert "llama3:latest" in models assert "mistral:latest" in models def test_check_model_available(self, provider): """Test model availability checking.""" import time provider._available_models = ["llama3:latest", "mistral:latest"] provider._models_cache_time = time.time() assert provider._check_model_available("llama3") is True assert provider._check_model_available("mistral") is True assert provider._check_model_available("qwen2") is False