Просмотр исходного кода

chore(backend-testing-01): create root conftest.py with shared fixtures

- pytest_configure for CI detection and markers
- pytest_collection_modifyitems to auto-skip hardware tests in CI
- async_client fixture for API testing with httpx
- mock_state fixture with comprehensive state defaults
- mock_connection fixture for serial/websocket mocking
- patterns_dir fixture for temporary test patterns
tuanchris 1 неделя назад
Родитель
Сommit
6b43ca971f
1 измененных файлов с 165 добавлено и 0 удалено
  1. 165 0
      tests/conftest.py

+ 165 - 0
tests/conftest.py

@@ -0,0 +1,165 @@
+"""
+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