Files
disbord/tests/fixtures/utils_fixtures.py
Travis Vasceannie 3acb779569 chore: remove .env.example and add new files for project structure
- Deleted .env.example file as it is no longer needed.
- Added .gitignore to manage ignored files and directories.
- Introduced CLAUDE.md for AI provider integration documentation.
- Created dev.sh for development setup and scripts.
- Updated Dockerfile and Dockerfile.production for improved build processes.
- Added multiple test files and directories for comprehensive testing.
- Introduced new utility and service files for enhanced functionality.
- Organized codebase with new directories and files for better maintainability.
2025-08-27 23:00:19 -04:00

739 lines
22 KiB
Python

"""
Test fixtures for utils components
Provides specialized fixtures for testing utils modules including:
- Mock audio data and files
- Mock Discord objects for permissions testing
- Mock AI prompt data
- Mock metrics data
- Mock configuration objects
- Error and exception scenarios
- Performance testing data
"""
import asyncio
import os
import struct
import tempfile
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List
from unittest.mock import AsyncMock, Mock
import discord
import numpy as np
import pytest
from utils.audio_processor import AudioConfig
from utils.exceptions import AudioProcessingError, ValidationError
class AudioTestData:
"""Factory for creating audio test data."""
@staticmethod
def create_sine_wave(
frequency: float = 440.0, duration: float = 1.0, sample_rate: int = 16000
) -> np.ndarray:
"""Create sine wave audio data."""
samples = int(duration * sample_rate)
t = np.linspace(0, duration, samples, False)
return np.sin(2 * np.pi * frequency * t).astype(np.float32)
@staticmethod
def create_white_noise(
duration: float = 1.0, sample_rate: int = 16000, amplitude: float = 0.1
) -> np.ndarray:
"""Create white noise audio data."""
samples = int(duration * sample_rate)
return (np.random.random(samples) - 0.5) * 2 * amplitude
@staticmethod
def create_silence(duration: float = 1.0, sample_rate: int = 16000) -> np.ndarray:
"""Create silent audio data."""
samples = int(duration * sample_rate)
return np.zeros(samples, dtype=np.float32)
@staticmethod
def create_pcm_bytes(audio_array: np.ndarray, sample_rate: int = 16000) -> bytes:
"""Convert audio array to PCM bytes."""
# Normalize and convert to 16-bit PCM
normalized = np.clip(audio_array * 32767, -32768, 32767).astype(np.int16)
return normalized.tobytes()
@staticmethod
def create_wav_header(
data_size: int, sample_rate: int = 16000, channels: int = 1
) -> bytes:
"""Create WAV file header."""
return (
b"RIFF"
+ struct.pack("<I", data_size + 36)
+ b"WAVE"
+ b"fmt "
+ struct.pack("<I", 16) # fmt chunk size
+ struct.pack("<H", 1) # PCM format
+ struct.pack("<H", channels)
+ struct.pack("<I", sample_rate)
+ struct.pack("<I", sample_rate * channels * 2) # byte rate
+ struct.pack("<H", channels * 2) # block align
+ struct.pack("<H", 16) # bits per sample
+ b"data"
+ struct.pack("<I", data_size)
)
class DiscordTestObjects:
"""Factory for creating mock Discord objects."""
@staticmethod
def create_mock_guild(
guild_id: int = 123456789, owner_id: int = 111111111, name: str = "Test Guild"
):
"""Create mock Discord guild."""
guild = Mock(spec=discord.Guild)
guild.id = guild_id
guild.owner_id = owner_id
guild.name = name
return guild
@staticmethod
def create_mock_member(
user_id: int = 222222222, username: str = "TestUser", **permissions
):
"""Create mock Discord member with permissions."""
member = Mock(spec=discord.Member)
member.id = user_id
member.name = username
member.display_name = username
# Create guild permissions
perms = Mock()
default_permissions = {
"administrator": False,
"manage_guild": False,
"manage_messages": False,
"manage_channels": False,
"kick_members": False,
"ban_members": False,
"manage_roles": False,
"connect": False,
"speak": False,
"use_voice_activation": False,
"read_messages": True,
"send_messages": True,
"embed_links": True,
"attach_files": True,
"use_slash_commands": True,
}
default_permissions.update(permissions)
for perm, value in default_permissions.items():
setattr(perms, perm, value)
member.guild_permissions = perms
return member
@staticmethod
def create_mock_voice_channel(
channel_id: int = 333333333, name: str = "Test Voice"
):
"""Create mock Discord voice channel."""
channel = Mock(spec=discord.VoiceChannel)
channel.id = channel_id
channel.name = name
def mock_permissions_for(member):
"""Mock permissions for member in channel."""
perms = Mock()
perms.connect = True
perms.speak = True
perms.use_voice_activation = True
return perms
channel.permissions_for = mock_permissions_for
return channel
@staticmethod
def create_mock_text_channel(channel_id: int = 444444444, name: str = "Test Text"):
"""Create mock Discord text channel."""
channel = Mock(spec=discord.TextChannel)
channel.id = channel_id
channel.name = name
return channel
class PromptsTestData:
"""Factory for creating prompt test data."""
@staticmethod
def create_quote_data(
quote: str = "This is a test quote that's quite funny!",
speaker_name: str = "TestUser",
**scores,
) -> Dict[str, Any]:
"""Create quote data for testing."""
default_scores = {
"funny_score": 7.5,
"dark_score": 2.1,
"silly_score": 6.8,
"suspicious_score": 1.0,
"asinine_score": 3.2,
"overall_score": 6.5,
}
default_scores.update(scores)
return {
"quote": quote,
"speaker_name": speaker_name,
"timestamp": datetime.now(timezone.utc).isoformat(),
**default_scores,
}
@staticmethod
def create_context_data(
conversation: str = "The group was discussing funny movies and this came up.",
laughter_duration: float = 3.5,
laughter_intensity: float = 0.8,
**extras,
) -> Dict[str, Any]:
"""Create context data for testing."""
data = {
"conversation": conversation,
"laughter_duration": laughter_duration,
"laughter_intensity": laughter_intensity,
"personality": "Known for witty humor and clever observations",
"recent_interactions": "Recently active in comedy discussions",
"recent_context": "Has been making witty comments all day",
}
data.update(extras)
return data
@staticmethod
def create_user_profile_data(
username: str = "ComedyUser", quote_count: int = 5
) -> Dict[str, Any]:
"""Create user profile data for personality analysis."""
quotes = []
for i in range(quote_count):
quotes.append(
{
"quote": f"This is test quote number {i+1}",
"funny_score": 5.0 + i,
"dark_score": 1.0 + (i * 0.5),
"silly_score": 6.0 + (i * 0.3),
"timestamp": (
datetime.now(timezone.utc) - timedelta(days=i)
).isoformat(),
}
)
return {
"username": username,
"quotes": quotes,
"avg_funny_score": 7.0,
"avg_dark_score": 2.5,
"avg_silly_score": 6.5,
"primary_humor_style": "witty",
"quote_frequency": 3.2,
"active_hours": [14, 15, 19, 20, 21],
"avg_quote_length": 65,
}
class MetricsTestData:
"""Factory for creating metrics test data."""
@staticmethod
def create_metric_events(count: int = 10) -> List[Dict[str, Any]]:
"""Create metric events for testing."""
events = []
base_time = datetime.now(timezone.utc)
metric_types = ["quotes_detected", "audio_processed", "ai_requests", "errors"]
for i in range(count):
events.append(
{
"name": metric_types[i % len(metric_types)],
"value": float(i + 1),
"labels": {
"guild_id": str(123456 + (i % 3)),
"component": f"component_{i % 4}",
"status": "success" if i % 4 != 3 else "error",
},
"timestamp": base_time - timedelta(minutes=i * 5),
}
)
return events
@staticmethod
def create_system_metrics() -> Dict[str, Any]:
"""Create system metrics for testing."""
return {
"memory_rss": 1024 * 1024 * 100, # 100MB
"memory_vms": 1024 * 1024 * 200, # 200MB
"cpu_percent": 15.5,
"num_fds": 150,
"num_threads": 25,
"uptime_seconds": 3600 * 24, # 1 day
}
@staticmethod
def create_prometheus_data() -> str:
"""Create sample Prometheus metrics data."""
return """# HELP discord_quotes_detected_total Total number of quotes detected
# TYPE discord_quotes_detected_total counter
discord_quotes_detected_total{guild_id="123456",speaker_type="user"} 42.0
# HELP discord_memory_usage_bytes Current memory usage in bytes
# TYPE discord_memory_usage_bytes gauge
discord_memory_usage_bytes{type="rss"} 104857600.0
# HELP discord_errors_total Total errors by type
# TYPE discord_errors_total counter
discord_errors_total{error_type="validation",component="audio_processor"} 3.0
"""
# Pytest fixtures using the test data factories
@pytest.fixture
def audio_test_data():
"""Provide AudioTestData factory."""
return AudioTestData
@pytest.fixture
def sample_sine_wave(audio_test_data):
"""Create sample sine wave audio."""
return audio_test_data.create_sine_wave(frequency=440, duration=2.0)
@pytest.fixture
def sample_audio_bytes(sample_sine_wave, audio_test_data):
"""Create sample audio as PCM bytes."""
return audio_test_data.create_pcm_bytes(sample_sine_wave)
@pytest.fixture
def sample_wav_file(sample_sine_wave, audio_test_data):
"""Create temporary WAV file with sample audio."""
pcm_data = audio_test_data.create_pcm_bytes(sample_sine_wave)
header = audio_test_data.create_wav_header(len(pcm_data))
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
f.write(header + pcm_data)
temp_path = f.name
yield temp_path
# Cleanup
if os.path.exists(temp_path):
os.unlink(temp_path)
@pytest.fixture
def audio_config():
"""Create AudioConfig instance for testing."""
return AudioConfig()
@pytest.fixture
def discord_objects():
"""Provide DiscordTestObjects factory."""
return DiscordTestObjects
@pytest.fixture
def mock_guild(discord_objects):
"""Create mock Discord guild."""
return discord_objects.create_mock_guild()
@pytest.fixture
def mock_owner_member(discord_objects, mock_guild):
"""Create mock guild owner member."""
return discord_objects.create_mock_member(
user_id=mock_guild.owner_id, username="GuildOwner"
)
@pytest.fixture
def mock_admin_member(discord_objects):
"""Create mock admin member."""
return discord_objects.create_mock_member(
user_id=555555555, username="AdminUser", administrator=True
)
@pytest.fixture
def mock_moderator_member(discord_objects):
"""Create mock moderator member."""
return discord_objects.create_mock_member(
user_id=666666666,
username="ModeratorUser",
manage_messages=True,
kick_members=True,
)
@pytest.fixture
def mock_regular_member(discord_objects):
"""Create mock regular member."""
return discord_objects.create_mock_member(
user_id=777777777, username="RegularUser", connect=True
)
@pytest.fixture
def mock_bot_member(discord_objects):
"""Create mock bot member with standard permissions."""
return discord_objects.create_mock_member(
user_id=888888888,
username="TestBot",
read_messages=True,
send_messages=True,
embed_links=True,
attach_files=True,
use_slash_commands=True,
)
@pytest.fixture
def mock_voice_channel(discord_objects):
"""Create mock voice channel."""
return discord_objects.create_mock_voice_channel()
@pytest.fixture
def mock_text_channel(discord_objects):
"""Create mock text channel."""
return discord_objects.create_mock_text_channel()
@pytest.fixture
def prompts_test_data():
"""Provide PromptsTestData factory."""
return PromptsTestData
@pytest.fixture
def sample_quote_data(prompts_test_data):
"""Create sample quote data."""
return prompts_test_data.create_quote_data()
@pytest.fixture
def sample_context_data(prompts_test_data):
"""Create sample context data."""
return prompts_test_data.create_context_data()
@pytest.fixture
def sample_user_profile(prompts_test_data):
"""Create sample user profile data."""
return prompts_test_data.create_user_profile_data()
@pytest.fixture
def metrics_test_data():
"""Provide MetricsTestData factory."""
return MetricsTestData
@pytest.fixture
def sample_metric_events(metrics_test_data):
"""Create sample metric events."""
return metrics_test_data.create_metric_events(20)
@pytest.fixture
def sample_system_metrics(metrics_test_data):
"""Create sample system metrics."""
return metrics_test_data.create_system_metrics()
@pytest.fixture
def sample_prometheus_data(metrics_test_data):
"""Create sample Prometheus data."""
return metrics_test_data.create_prometheus_data()
@pytest.fixture
def mock_subprocess_success():
"""Create mock successful subprocess result."""
result = Mock()
result.returncode = 0
result.stdout = "Success output"
result.stderr = ""
return result
@pytest.fixture
def mock_subprocess_failure():
"""Create mock failed subprocess result."""
result = Mock()
result.returncode = 1
result.stdout = "Some output"
result.stderr = "Error: Command failed"
return result
@pytest.fixture
def sample_exceptions():
"""Create sample exceptions for testing error handling."""
return {
"validation_error": ValidationError(
"Invalid input", "test_component", "test_operation"
),
"audio_error": AudioProcessingError(
"Audio processing failed", "audio_processor", "process_audio"
),
"discord_http_error": discord.HTTPException("HTTP request failed"),
"discord_forbidden": discord.Forbidden("Access denied"),
"connection_error": ConnectionError("Network connection failed"),
"timeout_error": asyncio.TimeoutError("Operation timed out"),
"value_error": ValueError("Invalid value provided"),
"file_not_found": FileNotFoundError("Required file not found"),
}
@pytest.fixture
def complex_metadata():
"""Create complex metadata for testing exception contexts."""
return {
"request_id": "req_12345",
"user_data": {
"id": 999999999,
"username": "TestUser",
"permissions": ["read", "write"],
},
"operation_context": {
"start_time": datetime.now(timezone.utc).isoformat(),
"retry_count": 2,
"timeout": 30.0,
},
"performance_metrics": {
"cpu_usage": 25.5,
"memory_usage": 1024 * 1024 * 50,
"processing_time": 1.234,
},
"flags": {"debug_enabled": True, "cache_hit": False, "background_task": True},
}
@pytest.fixture
def mock_ai_responses():
"""Create mock AI provider responses."""
return {
"analysis_response": {
"funny": 8.5,
"dark": 2.0,
"silly": 7.2,
"suspicious": 1.0,
"asinine": 3.5,
"reasoning": "The quote demonstrates clever wordplay with unexpected timing.",
"overall_assessment": "Highly amusing quote with good comedic timing.",
"confidence": 0.92,
},
"commentary_response": "That's the kind of humor that catches everyone off guard! 😄",
"personality_response": """This user demonstrates a consistent pattern of witty, observational humor.
They tend to find clever angles on everyday situations and have excellent timing with their comments.
Their humor style leans toward wordplay and situational comedy rather than dark or absurd humor.""",
}
@pytest.fixture
def performance_test_datasets():
"""Create datasets for performance testing."""
return {
"small_dataset": list(range(100)),
"medium_dataset": list(range(1000)),
"large_dataset": list(range(10000)),
"audio_samples": [
AudioTestData.create_sine_wave(freq, 0.1) for freq in [220, 440, 880, 1760]
],
"text_samples": [
f"This is test text sample number {i} with some content to process."
for i in range(500)
],
}
@pytest.fixture
async def async_context_manager():
"""Create async context manager for testing."""
class TestAsyncContextManager:
def __init__(self):
self.entered = False
self.exited = False
self.exception_handled = None
async def __aenter__(self):
self.entered = True
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
self.exited = True
self.exception_handled = exc_val
return False
return TestAsyncContextManager()
@pytest.fixture
def mock_discord_api_responses():
"""Create mock Discord API responses for testing."""
return {
"message_response": {
"id": "123456789",
"content": "Test message",
"author": {"id": "987654321", "username": "TestUser"},
"timestamp": datetime.now(timezone.utc).isoformat(),
},
"guild_response": {
"id": "111222333",
"name": "Test Guild",
"owner_id": "444555666",
"member_count": 150,
},
"channel_response": {
"id": "777888999",
"name": "general",
"type": 0, # Text channel
"guild_id": "111222333",
},
}
@pytest.fixture
def error_scenarios():
"""Create various error scenarios for testing."""
return {
"rate_limit_error": discord.RateLimited(retry_after=30.0),
"permission_denied": discord.Forbidden("Missing permissions"),
"not_found": discord.NotFound("Resource not found"),
"server_error": discord.HTTPException("Internal server error"),
"timeout_error": asyncio.TimeoutError("Request timed out"),
"validation_error": ValidationError(
"Invalid input format", "validator", "check_input"
),
"processing_error": AudioProcessingError(
"Failed to process audio", "audio_processor", "convert_format"
),
}
# Utility functions for test fixtures
def create_temp_directory():
"""Create temporary directory for test files."""
temp_dir = tempfile.mkdtemp(prefix="disbord_test_")
return temp_dir
def cleanup_temp_files(*file_paths):
"""Clean up temporary files created during testing."""
for file_path in file_paths:
if file_path and os.path.exists(file_path):
try:
os.unlink(file_path)
except OSError:
pass # Ignore cleanup errors
@pytest.fixture
def temp_directory():
"""Create temporary directory that's cleaned up after test."""
temp_dir = create_temp_directory()
yield temp_dir
# Cleanup
import shutil
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
@pytest.fixture(scope="session")
def audio_test_files():
"""Create audio test files for session-wide use."""
files = {}
temp_dir = create_temp_directory()
try:
# Create different types of audio files
sine_wave = AudioTestData.create_sine_wave(440, 1.0)
noise = AudioTestData.create_white_noise(1.0)
silence = AudioTestData.create_silence(1.0)
for name, audio_data in [
("sine", sine_wave),
("noise", noise),
("silence", silence),
]:
pcm_data = AudioTestData.create_pcm_bytes(audio_data)
header = AudioTestData.create_wav_header(len(pcm_data))
file_path = os.path.join(temp_dir, f"{name}.wav")
with open(file_path, "wb") as f:
f.write(header + pcm_data)
files[name] = file_path
yield files
finally:
# Cleanup
import shutil
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
# Mock factories for complex objects
class MockErrorHandlerFactory:
"""Factory for creating mock error handlers."""
@staticmethod
def create_mock_error_handler():
"""Create mock error handler for testing."""
handler = Mock()
handler.handle_error = Mock()
handler.get_error_category = Mock(return_value="test_category")
handler.get_error_severity = Mock(return_value="medium")
return handler
class MockMetricsCollectorFactory:
"""Factory for creating mock metrics collectors."""
@staticmethod
def create_mock_metrics_collector():
"""Create mock metrics collector for testing."""
collector = Mock()
collector.increment = Mock()
collector.observe_histogram = Mock()
collector.set_gauge = Mock()
collector.check_health = Mock(return_value={"status": "healthy"})
collector.export_metrics = AsyncMock(return_value="# Mock metrics data")
return collector
@pytest.fixture
def mock_error_handler():
"""Create mock error handler."""
return MockErrorHandlerFactory.create_mock_error_handler()
@pytest.fixture
def mock_metrics_collector():
"""Create mock metrics collector."""
return MockMetricsCollectorFactory.create_mock_metrics_collector()