opencompletion.com/tests/conftest.py

127 lines
4 KiB
Python

#!/usr/bin/env python3
"""
pytest configuration and fixtures for OpenCompletion testing
Sets up common test environment variables and fixtures used across all tests.
"""
import sys
import os
# =============================================================================
# CRITICAL: Mock tiktoken BEFORE any other imports
# tiktoken tries to download encoding files over HTTPS which conflicts
# with gevent's monkey-patching of SSL, causing RecursionError
# =============================================================================
class MockTiktokenEncoding:
"""Mock tiktoken encoding that doesn't make network requests"""
def encode(self, text):
# Simple approximation: ~4 chars per token
return list(range(len(text) // 4 + 1))
class MockTiktoken:
"""Mock tiktoken module"""
_encoding = MockTiktokenEncoding()
@staticmethod
def encoding_for_model(model_name):
return MockTiktoken._encoding
@staticmethod
def get_encoding(encoding_name):
return MockTiktoken._encoding
# Insert mock tiktoken into sys.modules BEFORE any imports
# Use the class itself (not an instance) so patching works correctly
if 'tiktoken' not in sys.modules:
sys.modules['tiktoken'] = MockTiktoken
def pytest_configure(config):
"""
Called early in pytest startup, before test collection.
Ensures tiktoken is mocked before any test imports happen.
"""
if 'tiktoken' not in sys.modules:
sys.modules['tiktoken'] = MockTiktoken
else:
# If tiktoken was already imported, patch its functions
tiktoken_mod = sys.modules['tiktoken']
tiktoken_mod.encoding_for_model = MockTiktoken.encoding_for_model
tiktoken_mod.get_encoding = MockTiktoken.get_encoding
# =============================================================================
# Now safe to do other imports
# =============================================================================
import pytest
import tempfile
from unittest.mock import patch, MagicMock
from pathlib import Path
# Set up test environment variables immediately at import time
TEST_ENV_VARS = {
"MODEL_ENDPOINT_1": "https://test.api",
"MODEL_NAME_1": "test-model",
"MODEL_KEY_1": "test-key",
"TESTING": "1",
}
# Apply environment variables immediately for import
os.environ.update(TEST_ENV_VARS)
@pytest.fixture(scope="session", autouse=True)
def setup_test_environment():
"""Set up test environment variables for all tests"""
with patch.dict(os.environ, TEST_ENV_VARS):
yield
@pytest.fixture
def mock_openai_client():
"""Mock OpenAI client for testing"""
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.choices[0].message.content.strip.return_value = "test response"
mock_client.chat.completions.create.return_value = mock_response
return mock_client
@pytest.fixture
def mock_s3_client():
"""Mock S3 client for testing"""
mock_client = MagicMock()
mock_response = {"Body": MagicMock()}
mock_response["Body"].read.return_value.decode.return_value = "test: content"
mock_client.get_object.return_value = mock_response
return mock_client
@pytest.fixture(scope="function")
def test_app():
"""Create a test Flask app with in-memory database"""
# Import here to avoid circular dependencies
import app as app_module
from models import db
# Create a temporary directory for instance path
with tempfile.TemporaryDirectory() as tmpdir:
app_module.app.config["TESTING"] = True
app_module.app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
app_module.app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app_module.app.config["WTF_CSRF_ENABLED"] = False
app_module.app.instance_path = tmpdir
with app_module.app.app_context():
# Recreate all tables with test config
db.drop_all()
db.create_all()
yield app_module.app
db.session.remove()
db.drop_all()