""" Root conftest.py - Shared fixtures for all tests. This file provides: - CI environment detection for auto-skipping hardware tests - AsyncClient fixture for API testing - Mock state fixture for isolated testing """ import os import pytest from unittest.mock import MagicMock, AsyncMock def pytest_configure(config): """Configure pytest with custom markers and CI detection.""" # Register custom markers config.addinivalue_line( "markers", "hardware: marks tests requiring real hardware (skip in CI)" ) config.addinivalue_line( "markers", "slow: marks slow tests" ) def pytest_collection_modifyitems(config, items): """Auto-skip hardware tests when CI=true environment variable is set.""" if os.environ.get("CI"): skip_hardware = pytest.mark.skip(reason="Hardware not available in CI") for item in items: if "hardware" in item.keywords: item.add_marker(skip_hardware) @pytest.fixture async def async_client(): """Async HTTP client for testing API endpoints. Uses httpx AsyncClient with ASGITransport to test FastAPI app directly without starting a server. """ from httpx import ASGITransport, AsyncClient from main import app async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: yield client @pytest.fixture def mock_state(): """Mock global state object for isolated testing. Returns a MagicMock configured with common defaults to simulate the application state without affecting real state. """ mock = MagicMock() # Connection mock mock.conn = MagicMock() mock.conn.is_connected.return_value = False mock.port = None mock.is_connected = False # Pattern execution state mock.current_playing_file = None mock.is_running = False mock.pause_requested = False mock.stop_requested = False mock.skip_requested = False mock.execution_progress = None mock.is_homing = False mock.is_clearing = False # Position state mock.current_theta = 0.0 mock.current_rho = 0.0 mock.machine_x = 0.0 mock.machine_y = 0.0 # Speed and settings mock.speed = 100 mock.clear_pattern_speed = None mock.table_type = "dune_weaver" mock.table_type_override = None mock.homing = 0 # Playlist state mock.current_playlist = None mock.current_playlist_name = None mock.current_playlist_index = None mock.playlist_mode = None mock.pause_time_remaining = 0 mock.original_pause_time = None # LED state mock.led_controller = None mock.led_provider = "none" mock.wled_ip = None mock.dw_led_idle_effect = "solid" mock.dw_led_playing_effect = "rainbow" mock.dw_led_idle_timeout_enabled = False mock.dw_led_idle_timeout_minutes = 30 # Scheduled pause mock.scheduled_pause_enabled = False mock.scheduled_pause_time_slots = [] mock.scheduled_pause_control_wled = False mock.scheduled_pause_finish_pattern = False mock.scheduled_pause_timezone = None # Steps and gear ratio mock.x_steps_per_mm = 200.0 mock.y_steps_per_mm = 287.0 mock.gear_ratio = 10.0 # Auto-home settings mock.auto_home_enabled = False mock.auto_home_after_patterns = 10 mock.patterns_since_last_home = 0 # Custom clear patterns mock.custom_clear_from_out = None mock.custom_clear_from_in = None # Homing offset mock.angular_homing_offset_degrees = 0.0 # Methods mock.save = MagicMock() mock.get_stop_event = MagicMock(return_value=None) mock.get_skip_event = MagicMock(return_value=None) mock.wait_for_interrupt = AsyncMock(return_value='timeout') return mock @pytest.fixture def mock_connection(): """Mock connection object for testing hardware communication. Returns a MagicMock configured to simulate serial/websocket connection. """ mock = MagicMock() mock.is_connected.return_value = True mock.send = MagicMock() mock.readline = MagicMock(return_value="ok") mock.in_waiting = MagicMock(return_value=0) mock.flush = MagicMock() mock.close = MagicMock() mock.reset_input_buffer = MagicMock() return mock @pytest.fixture def patterns_dir(tmp_path): """Create a temporary patterns directory for testing. Returns the path to a temporary directory that can be used for pattern file operations during tests. """ patterns = tmp_path / "patterns" patterns.mkdir() return patterns