|
|
@@ -0,0 +1,189 @@
|
|
|
+"""
|
|
|
+Unit test conftest.py - Fixtures specific to unit tests.
|
|
|
+
|
|
|
+Provides fixtures for mocking FastAPI dependencies and isolating tests
|
|
|
+from real state and hardware connections.
|
|
|
+"""
|
|
|
+import pytest
|
|
|
+from unittest.mock import MagicMock, AsyncMock, patch
|
|
|
+
|
|
|
+
|
|
|
+@pytest.fixture
|
|
|
+def mock_state_unit():
|
|
|
+ """Mock state for unit tests with common defaults.
|
|
|
+
|
|
|
+ This is a more comprehensive mock than the root conftest version,
|
|
|
+ specifically designed for API endpoint testing where we need to
|
|
|
+ control the application state precisely.
|
|
|
+ """
|
|
|
+ mock = MagicMock()
|
|
|
+
|
|
|
+ # Connection mock
|
|
|
+ mock.conn = MagicMock()
|
|
|
+ mock.conn.is_connected.return_value = False
|
|
|
+ mock.port = None
|
|
|
+ mock.is_connected = False
|
|
|
+ mock.preferred_port = None
|
|
|
+
|
|
|
+ # 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.hyperion_ip = None
|
|
|
+ mock.hyperion_port = 19444
|
|
|
+ mock.dw_led_num_leds = 60
|
|
|
+ mock.dw_led_gpio_pin = 18
|
|
|
+ mock.dw_led_pixel_order = "GRB"
|
|
|
+ mock.dw_led_brightness = 50
|
|
|
+ mock.dw_led_speed = 128
|
|
|
+ mock.dw_led_intensity = 128
|
|
|
+ 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
|
|
|
+ mock.dw_led_last_activity_time = 0
|
|
|
+
|
|
|
+ # 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
|
|
|
+
|
|
|
+ # App settings
|
|
|
+ mock.app_name = "Dune Weaver"
|
|
|
+ mock.custom_logo_path = None
|
|
|
+
|
|
|
+ # MQTT settings
|
|
|
+ mock.mqtt_enabled = False
|
|
|
+ mock.mqtt_broker = None
|
|
|
+ mock.mqtt_port = 1883
|
|
|
+ mock.mqtt_username = None
|
|
|
+ mock.mqtt_password = None
|
|
|
+ mock.mqtt_topic_prefix = "dune_weaver"
|
|
|
+
|
|
|
+ # Table info
|
|
|
+ mock.table_id = None
|
|
|
+ mock.table_name = None
|
|
|
+ mock.known_tables = []
|
|
|
+
|
|
|
+ # 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')
|
|
|
+ mock.pause_condition = MagicMock()
|
|
|
+ mock.pause_condition.__enter__ = MagicMock()
|
|
|
+ mock.pause_condition.__exit__ = MagicMock()
|
|
|
+ mock.pause_condition.notify_all = MagicMock()
|
|
|
+
|
|
|
+ return mock
|
|
|
+
|
|
|
+
|
|
|
+@pytest.fixture
|
|
|
+def mock_connection_unit():
|
|
|
+ """Mock connection for unit tests.
|
|
|
+
|
|
|
+ Provides a connection mock that simulates a connected device
|
|
|
+ without requiring actual hardware.
|
|
|
+ """
|
|
|
+ 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 app_with_mocked_state(mock_state_unit):
|
|
|
+ """Fixture that patches state module before importing app.
|
|
|
+
|
|
|
+ This ensures the app uses mocked state for all operations.
|
|
|
+ Must be used before creating async_client.
|
|
|
+ """
|
|
|
+ with patch("modules.core.state.state", mock_state_unit):
|
|
|
+ with patch("modules.core.pattern_manager.state", mock_state_unit):
|
|
|
+ with patch("modules.core.playlist_manager.state", mock_state_unit):
|
|
|
+ with patch("modules.connection.connection_manager.state", mock_state_unit):
|
|
|
+ from main import app
|
|
|
+ yield app, mock_state_unit
|
|
|
+
|
|
|
+
|
|
|
+@pytest.fixture
|
|
|
+async def async_client_with_mocked_state(app_with_mocked_state):
|
|
|
+ """AsyncClient with mocked state for isolated API testing.
|
|
|
+
|
|
|
+ This fixture combines the app patching with the async client creation.
|
|
|
+ """
|
|
|
+ from httpx import ASGITransport, AsyncClient
|
|
|
+
|
|
|
+ app, mock_state = app_with_mocked_state
|
|
|
+
|
|
|
+ async with AsyncClient(
|
|
|
+ transport=ASGITransport(app=app),
|
|
|
+ base_url="http://test"
|
|
|
+ ) as client:
|
|
|
+ yield client, mock_state
|
|
|
+
|
|
|
+
|
|
|
+@pytest.fixture
|
|
|
+def cleanup_app_overrides():
|
|
|
+ """Fixture to ensure app.dependency_overrides is cleaned up after tests."""
|
|
|
+ from main import app
|
|
|
+
|
|
|
+ yield
|
|
|
+
|
|
|
+ # Cleanup after test
|
|
|
+ app.dependency_overrides.clear()
|