conftest.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. """
  2. Root conftest.py - Shared fixtures for all tests.
  3. This file provides:
  4. - CI environment detection for auto-skipping hardware tests
  5. - AsyncClient fixture for API testing
  6. - Mock state fixture for isolated testing
  7. """
  8. import os
  9. import pytest
  10. from unittest.mock import MagicMock, AsyncMock
  11. def pytest_configure(config):
  12. """Configure pytest with custom markers and CI detection."""
  13. # Register custom markers
  14. config.addinivalue_line(
  15. "markers", "hardware: marks tests requiring real hardware (skip in CI)"
  16. )
  17. config.addinivalue_line(
  18. "markers", "slow: marks slow tests"
  19. )
  20. def pytest_collection_modifyitems(config, items):
  21. """Auto-skip hardware tests when CI=true environment variable is set."""
  22. if os.environ.get("CI"):
  23. skip_hardware = pytest.mark.skip(reason="Hardware not available in CI")
  24. for item in items:
  25. if "hardware" in item.keywords:
  26. item.add_marker(skip_hardware)
  27. @pytest.fixture
  28. async def async_client():
  29. """Async HTTP client for testing API endpoints.
  30. Uses httpx AsyncClient with ASGITransport to test FastAPI app directly
  31. without starting a server.
  32. """
  33. from httpx import ASGITransport, AsyncClient
  34. from main import app
  35. async with AsyncClient(
  36. transport=ASGITransport(app=app),
  37. base_url="http://test"
  38. ) as client:
  39. yield client
  40. @pytest.fixture
  41. def mock_state():
  42. """Mock global state object for isolated testing.
  43. Returns a MagicMock configured with common defaults to simulate
  44. the application state without affecting real state.
  45. """
  46. mock = MagicMock()
  47. # Connection mock
  48. mock.conn = MagicMock()
  49. mock.conn.is_connected.return_value = False
  50. mock.port = None
  51. mock.is_connected = False
  52. # Pattern execution state
  53. mock.current_playing_file = None
  54. mock.is_running = False
  55. mock.pause_requested = False
  56. mock.stop_requested = False
  57. mock.skip_requested = False
  58. mock.execution_progress = None
  59. mock.is_homing = False
  60. mock.is_clearing = False
  61. # Position state
  62. mock.current_theta = 0.0
  63. mock.current_rho = 0.0
  64. mock.machine_x = 0.0
  65. mock.machine_y = 0.0
  66. # Speed and settings
  67. mock.speed = 100
  68. mock.clear_pattern_speed = None
  69. mock.table_type = "dune_weaver"
  70. mock.table_type_override = None
  71. mock.homing = 0
  72. # Playlist state
  73. mock.current_playlist = None
  74. mock.current_playlist_name = None
  75. mock.current_playlist_index = None
  76. mock.playlist_mode = None
  77. mock.pause_time_remaining = 0
  78. mock.original_pause_time = None
  79. # LED state
  80. mock.led_controller = None
  81. mock.led_provider = "none"
  82. mock.wled_ip = None
  83. mock.dw_led_idle_effect = "solid"
  84. mock.dw_led_playing_effect = "rainbow"
  85. mock.dw_led_idle_timeout_enabled = False
  86. mock.dw_led_idle_timeout_minutes = 30
  87. # Scheduled pause
  88. mock.scheduled_pause_enabled = False
  89. mock.scheduled_pause_time_slots = []
  90. mock.scheduled_pause_control_wled = False
  91. mock.scheduled_pause_finish_pattern = False
  92. mock.scheduled_pause_timezone = None
  93. # Steps and gear ratio
  94. mock.x_steps_per_mm = 200.0
  95. mock.y_steps_per_mm = 287.0
  96. mock.gear_ratio = 10.0
  97. # Auto-home settings
  98. mock.auto_home_enabled = False
  99. mock.auto_home_after_patterns = 10
  100. mock.patterns_since_last_home = 0
  101. # Custom clear patterns
  102. mock.custom_clear_from_out = None
  103. mock.custom_clear_from_in = None
  104. # Homing offset
  105. mock.angular_homing_offset_degrees = 0.0
  106. # Methods
  107. mock.save = MagicMock()
  108. mock.get_stop_event = MagicMock(return_value=None)
  109. mock.get_skip_event = MagicMock(return_value=None)
  110. mock.wait_for_interrupt = AsyncMock(return_value='timeout')
  111. return mock
  112. @pytest.fixture
  113. def mock_connection():
  114. """Mock connection object for testing hardware communication.
  115. Returns a MagicMock configured to simulate serial/websocket connection.
  116. """
  117. mock = MagicMock()
  118. mock.is_connected.return_value = True
  119. mock.send = MagicMock()
  120. mock.readline = MagicMock(return_value="ok")
  121. mock.in_waiting = MagicMock(return_value=0)
  122. mock.flush = MagicMock()
  123. mock.close = MagicMock()
  124. mock.reset_input_buffer = MagicMock()
  125. return mock
  126. @pytest.fixture
  127. def patterns_dir(tmp_path):
  128. """Create a temporary patterns directory for testing.
  129. Returns the path to a temporary directory that can be used
  130. for pattern file operations during tests.
  131. """
  132. patterns = tmp_path / "patterns"
  133. patterns.mkdir()
  134. return patterns