""" pytest configuration and shared fixtures. """ import asyncio import pytest import pytest_asyncio from typing import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker from fastapi.testclient import TestClient from src.tempering_machine.shared.database import Base, get_db from src.tempering_machine.shared.config import settings from src.tempering_machine.services.web.main import app from src.tempering_machine.services.hardware.hardware_manager import hardware_manager from src.tempering_machine.services.safety.safety_monitor import safety_monitor from src.tempering_machine.services.recipe.recipe_controller import recipe_controller # Test database URL TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:" @pytest.fixture(scope="session") def event_loop(): """Create event loop for the test session.""" loop = asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() @pytest_asyncio.fixture async def test_db_engine(): """Create test database engine.""" engine = create_async_engine( TEST_DATABASE_URL, echo=False, future=True ) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield engine async with engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await engine.dispose() @pytest_asyncio.fixture async def test_db_session(test_db_engine) -> AsyncGenerator[AsyncSession, None]: """Create test database session.""" async_session = sessionmaker( test_db_engine, class_=AsyncSession, expire_on_commit=False ) async with async_session() as session: yield session @pytest.fixture def test_client(test_db_session): """Create test client with database override.""" def override_get_db(): return test_db_session app.dependency_overrides[get_db] = override_get_db with TestClient(app) as client: yield client app.dependency_overrides.clear() @pytest.fixture def mock_hardware_manager(): """Mock hardware manager for testing.""" mock = MagicMock() mock.initialize = AsyncMock(return_value=True) mock.shutdown = AsyncMock(return_value=True) mock.get_hardware_status = MagicMock() mock.get_all_temperatures = AsyncMock(return_value={}) mock.get_temperature = AsyncMock(return_value=None) mock.is_safe_to_operate = AsyncMock(return_value=(True, [])) mock.emergency_stop = AsyncMock(return_value=True) mock.set_motor_state = AsyncMock(return_value=True) mock.set_heater_state = AsyncMock(return_value=True) return mock @pytest.fixture def mock_safety_monitor(): """Mock safety monitor for testing.""" mock = MagicMock() mock.initialize = AsyncMock(return_value=True) mock.shutdown = AsyncMock(return_value=True) mock.get_active_alarms = MagicMock(return_value=[]) mock.get_alarm_summary = MagicMock(return_value={ "total_active_alarms": 0, "active_by_priority": {"critical": 0, "high": 0, "medium": 0, "low": 0}, "emergency_stop_active": False, "last_safety_check": "2025-01-08T12:00:00", }) mock.acknowledge_alarm = AsyncMock(return_value=True) return mock @pytest.fixture def mock_recipe_controller(): """Mock recipe controller for testing.""" mock = MagicMock() mock.start_recipe = AsyncMock(return_value="test-session-id") mock.stop_recipe = AsyncMock(return_value=True) mock.pause_recipe = AsyncMock(return_value=True) mock.resume_recipe = AsyncMock(return_value=True) mock.emergency_stop = AsyncMock(return_value=True) mock.get_process_status = AsyncMock(return_value=None) mock.get_active_sessions = AsyncMock(return_value=[]) return mock @pytest.fixture def sample_recipe_data(): """Sample recipe data for testing.""" return { "name": "Test Recipe", "description": "A test recipe for unit testing", "heating_goal": 46.0, "cooling_goal": 27.0, "pouring_goal": 30.0, "tank_temp": 45.0, "fountain_temp": 32.0, "mixer_enabled": True, "fountain_enabled": True, "mold_heater_enabled": False, "vibration_enabled": False, "vib_heater_enabled": False, "pedal_control_enabled": True, "pedal_on_time": 2.0, "pedal_off_time": 3.0, } @pytest.fixture def sample_temperature_reading(): """Sample temperature reading for testing.""" from datetime import datetime from src.tempering_machine.services.hardware.hardware_manager import TemperatureReading return TemperatureReading( value=25.5, timestamp=datetime.now(), sensor_name="tank_bottom", units="°C", is_valid=True, error_message=None ) @pytest.fixture def sample_hardware_status(): """Sample hardware status for testing.""" from datetime import datetime from src.tempering_machine.services.hardware.hardware_manager import ( HardwareStatus, TemperatureReading, MotorStatus, MotorState, SafetyStatus, ComponentStatus ) return HardwareStatus( temperatures={ "tank_bottom": TemperatureReading( value=25.5, timestamp=datetime.now(), sensor_name="tank_bottom", units="°C", is_valid=True ), "fountain": TemperatureReading( value=30.2, timestamp=datetime.now(), sensor_name="fountain", units="°C", is_valid=True ) }, motors={ "mixer_motor": MotorStatus( name="mixer_motor", state=MotorState.STOPPED, is_enabled=False ) }, safety=SafetyStatus( emergency_stop_active=False, cover_sensor_closed=True, temperature_alarms=[], current_alarms=[], last_safety_check=datetime.now() ), communication_health=95.0, system_status=ComponentStatus.ONLINE, last_update=datetime.now() ) @pytest.fixture(autouse=True) async def setup_test_environment(monkeypatch): """Set up test environment with mocked services.""" # Override settings for testing monkeypatch.setattr(settings, "environment", "testing") monkeypatch.setattr(settings, "debug", True) monkeypatch.setattr(settings.database, "url", TEST_DATABASE_URL) # Prevent actual hardware initialization during tests monkeypatch.setattr(hardware_manager, "initialize", AsyncMock(return_value=True)) monkeypatch.setattr(safety_monitor, "initialize", AsyncMock(return_value=True)) # Pytest markers pytest_plugins = ["pytest_asyncio"]