From c496c53ff712e1cc5660a938e7a721e19b660bb6 Mon Sep 17 00:00:00 2001 From: Cyber MacGeddon Date: Fri, 11 Jul 2025 16:51:06 +0100 Subject: [PATCH] Commonality extracted --- .../test_text_completion/common/__init__.py | 3 + .../common/base_test_cases.py | 69 +++++++ .../common/mock_helpers.py | 53 ++++++ tests/unit/test_text_completion/conftest.py | 175 ++++++++++-------- 4 files changed, 225 insertions(+), 75 deletions(-) create mode 100644 tests/unit/test_text_completion/common/__init__.py create mode 100644 tests/unit/test_text_completion/common/base_test_cases.py create mode 100644 tests/unit/test_text_completion/common/mock_helpers.py diff --git a/tests/unit/test_text_completion/common/__init__.py b/tests/unit/test_text_completion/common/__init__.py new file mode 100644 index 00000000..accffaae --- /dev/null +++ b/tests/unit/test_text_completion/common/__init__.py @@ -0,0 +1,3 @@ +""" +Common utilities for text completion tests +""" \ No newline at end of file diff --git a/tests/unit/test_text_completion/common/base_test_cases.py b/tests/unit/test_text_completion/common/base_test_cases.py new file mode 100644 index 00000000..ea562552 --- /dev/null +++ b/tests/unit/test_text_completion/common/base_test_cases.py @@ -0,0 +1,69 @@ +""" +Base test patterns that can be reused across different text completion models +""" + +from abc import ABC, abstractmethod +from unittest.mock import AsyncMock, MagicMock, patch +from unittest import IsolatedAsyncioTestCase + + +class BaseTextCompletionTestCase(IsolatedAsyncioTestCase, ABC): + """ + Base test class for text completion processors + Provides common test patterns that can be reused + """ + + @abstractmethod + def get_processor_class(self): + """Return the processor class to test""" + pass + + @abstractmethod + def get_base_config(self): + """Return base configuration for the processor""" + pass + + @abstractmethod + def get_mock_patches(self): + """Return list of patch decorators for mocking dependencies""" + pass + + def create_base_config(self, **overrides): + """Create base config with optional overrides""" + config = self.get_base_config() + config.update(overrides) + return config + + def create_mock_llm_result(self, text="Test response", in_token=10, out_token=5): + """Create a mock LLM result""" + from trustgraph.base import LlmResult + return LlmResult(text=text, in_token=in_token, out_token=out_token) + + +class CommonTestPatterns: + """ + Common test patterns that can be used across different models + """ + + @staticmethod + def basic_initialization_test_pattern(test_instance): + """ + Test pattern for basic processor initialization + test_instance should be a BaseTextCompletionTestCase + """ + # This would contain the common pattern for initialization testing + pass + + @staticmethod + def successful_generation_test_pattern(test_instance): + """ + Test pattern for successful content generation + """ + pass + + @staticmethod + def error_handling_test_pattern(test_instance): + """ + Test pattern for error handling + """ + pass \ No newline at end of file diff --git a/tests/unit/test_text_completion/common/mock_helpers.py b/tests/unit/test_text_completion/common/mock_helpers.py new file mode 100644 index 00000000..5fbb0db9 --- /dev/null +++ b/tests/unit/test_text_completion/common/mock_helpers.py @@ -0,0 +1,53 @@ +""" +Common mocking utilities for text completion tests +""" + +from unittest.mock import AsyncMock, MagicMock + + +class CommonMocks: + """Common mock objects used across text completion tests""" + + @staticmethod + def create_mock_async_processor_init(): + """Create mock for AsyncProcessor.__init__""" + mock = MagicMock() + mock.return_value = None + return mock + + @staticmethod + def create_mock_llm_service_init(): + """Create mock for LlmService.__init__""" + mock = MagicMock() + mock.return_value = None + return mock + + @staticmethod + def create_mock_response(text="Test response", prompt_tokens=10, completion_tokens=5): + """Create a mock response object""" + response = MagicMock() + response.text = text + response.usage_metadata.prompt_token_count = prompt_tokens + response.usage_metadata.candidates_token_count = completion_tokens + return response + + @staticmethod + def create_basic_config(): + """Create basic config with required fields""" + return { + 'concurrency': 1, + 'taskgroup': AsyncMock(), + 'id': 'test-processor' + } + + +class MockPatches: + """Common patch decorators for different services""" + + @staticmethod + def get_base_patches(): + """Get patches that are common to all processors""" + return [ + 'trustgraph.base.async_processor.AsyncProcessor.__init__', + 'trustgraph.base.llm_service.LlmService.__init__' + ] \ No newline at end of file diff --git a/tests/unit/test_text_completion/conftest.py b/tests/unit/test_text_completion/conftest.py index 9bd96981..ea8ab6e0 100644 --- a/tests/unit/test_text_completion/conftest.py +++ b/tests/unit/test_text_completion/conftest.py @@ -4,49 +4,19 @@ Pytest configuration and fixtures for text completion tests import pytest from unittest.mock import MagicMock, AsyncMock -from trustgraph.schema import TextCompletionRequest, TextCompletionResponse from trustgraph.base import LlmResult -@pytest.fixture -def mock_vertexai_credentials(): - """Mock Google Cloud service account credentials""" - return MagicMock() +# === Common Fixtures for All Text Completion Models === @pytest.fixture -def mock_vertexai_model(): - """Mock VertexAI GenerativeModel""" - mock_model = MagicMock() - mock_response = MagicMock() - mock_response.text = "Test response" - mock_response.usage_metadata.prompt_token_count = 10 - mock_response.usage_metadata.candidates_token_count = 5 - mock_model.generate_content.return_value = mock_response - return mock_model - - -@pytest.fixture -def sample_text_completion_request(): - """Sample TextCompletionRequest for testing""" - return TextCompletionRequest( - id="test-request-id", - prompt="Test prompt", - system="Test system prompt", - temperature=0.7, - max_output=1024 - ) - - -@pytest.fixture -def sample_text_completion_response(): - """Sample TextCompletionResponse for testing""" - return TextCompletionResponse( - id="test-response-id", - response="Test response", - in_token=10, - out_token=5, - model="gemini-2.0-flash-001" - ) +def base_processor_config(): + """Base configuration required by all processors""" + return { + 'concurrency': 1, + 'taskgroup': AsyncMock(), + 'id': 'test-processor' + } @pytest.fixture @@ -60,16 +30,19 @@ def sample_llm_result(): @pytest.fixture -def vertexai_processor_config(): - """Default configuration for VertexAI processor""" - return { - 'region': 'us-central1', - 'model': 'gemini-2.0-flash-001', - 'temperature': 0.0, - 'max_output': 8192, - 'private_key': 'private.json', - 'concurrency': 1 - } +def mock_async_processor_init(): + """Mock AsyncProcessor.__init__ to avoid infrastructure requirements""" + mock = MagicMock() + mock.return_value = None + return mock + + +@pytest.fixture +def mock_llm_service_init(): + """Mock LlmService.__init__ to avoid infrastructure requirements""" + mock = MagicMock() + mock.return_value = None + return mock @pytest.fixture @@ -92,21 +65,66 @@ def mock_pulsar_producer(): return AsyncMock() +@pytest.fixture(autouse=True) +def mock_env_vars(monkeypatch): + """Mock environment variables for testing""" + monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project") + monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/test-credentials.json") + + @pytest.fixture -def mock_flow_processor_config(): - """Mock flow processor configuration""" - return { - 'service_id': 'test-vertexai-service', - 'flow_name': 'test-flow', - 'consumer_name': 'test-consumer' - } +def mock_async_context_manager(): + """Mock async context manager for testing""" + class MockAsyncContextManager: + def __init__(self, return_value): + self.return_value = return_value + + async def __aenter__(self): + return self.return_value + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + return MockAsyncContextManager + + +# === VertexAI Specific Fixtures === + +@pytest.fixture +def mock_vertexai_credentials(): + """Mock Google Cloud service account credentials""" + return MagicMock() + + +@pytest.fixture +def mock_vertexai_model(): + """Mock VertexAI GenerativeModel""" + mock_model = MagicMock() + mock_response = MagicMock() + mock_response.text = "Test response" + mock_response.usage_metadata.prompt_token_count = 10 + mock_response.usage_metadata.candidates_token_count = 5 + mock_model.generate_content.return_value = mock_response + return mock_model + + +@pytest.fixture +def vertexai_processor_config(base_processor_config): + """Default configuration for VertexAI processor""" + config = base_processor_config.copy() + config.update({ + 'region': 'us-central1', + 'model': 'gemini-2.0-flash-001', + 'temperature': 0.0, + 'max_output': 8192, + 'private_key': 'private.json' + }) + return config @pytest.fixture def mock_safety_settings(): """Mock safety settings for VertexAI""" - from unittest.mock import MagicMock - safety_settings = [] for i in range(4): # 4 safety categories setting = MagicMock() @@ -136,24 +154,31 @@ def mock_vertexai_exception(): return ResourceExhausted("Test resource exhausted error") -@pytest.fixture(autouse=True) -def mock_env_vars(monkeypatch): - """Mock environment variables for testing""" - monkeypatch.setenv("GOOGLE_CLOUD_PROJECT", "test-project") - monkeypatch.setenv("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/test-credentials.json") +# === Ollama Specific Fixtures (for next implementation) === + +@pytest.fixture +def ollama_processor_config(base_processor_config): + """Default configuration for Ollama processor""" + config = base_processor_config.copy() + config.update({ + 'model': 'llama2', + 'temperature': 0.0, + 'max_output': 8192, + 'host': 'localhost', + 'port': 11434 + }) + return config @pytest.fixture -def mock_async_context_manager(): - """Mock async context manager for testing""" - class MockAsyncContextManager: - def __init__(self, return_value): - self.return_value = return_value - - async def __aenter__(self): - return self.return_value - - async def __aexit__(self, exc_type, exc_val, exc_tb): - pass - - return MockAsyncContextManager +def mock_ollama_client(): + """Mock Ollama client""" + mock_client = MagicMock() + mock_response = { + 'response': 'Test response from Ollama', + 'done': True, + 'eval_count': 5, + 'prompt_eval_count': 10 + } + mock_client.generate.return_value = mock_response + return mock_client \ No newline at end of file