conftest.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. """
  2. Integration test fixtures - Hardware detection and setup.
  3. These fixtures help determine if real hardware is available
  4. and provide setup/teardown for hardware-dependent tests.
  5. """
  6. import pytest
  7. import serial.tools.list_ports
  8. def pytest_addoption(parser):
  9. """Add --run-hardware option to pytest CLI."""
  10. parser.addoption(
  11. "--run-hardware",
  12. action="store_true",
  13. default=False,
  14. help="Run tests that require real hardware connection"
  15. )
  16. @pytest.fixture
  17. def run_hardware(request):
  18. """Check if hardware tests should run."""
  19. return request.config.getoption("--run-hardware")
  20. @pytest.fixture
  21. def available_serial_ports():
  22. """Return list of available serial ports on this machine.
  23. Filters out known non-hardware ports like debug consoles.
  24. """
  25. IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
  26. ports = serial.tools.list_ports.comports()
  27. return [port.device for port in ports if port.device not in IGNORE_PORTS]
  28. @pytest.fixture
  29. def hardware_port(available_serial_ports, run_hardware):
  30. """Get a hardware port for testing, or skip if not available.
  31. This fixture:
  32. 1. Checks if --run-hardware flag was passed
  33. 2. Checks if any serial ports are available
  34. 3. Returns the first available port or skips the test
  35. """
  36. if not run_hardware:
  37. pytest.skip("Hardware tests disabled (use --run-hardware to enable)")
  38. if not available_serial_ports:
  39. pytest.skip("No serial ports available for hardware testing")
  40. # Prefer USB ports over built-in ports
  41. usb_ports = [p for p in available_serial_ports if 'usb' in p.lower() or 'USB' in p]
  42. if usb_ports:
  43. return usb_ports[0]
  44. return available_serial_ports[0]
  45. @pytest.fixture
  46. def serial_connection(hardware_port):
  47. """Create a real serial connection for testing.
  48. This fixture establishes an actual serial connection to the hardware.
  49. The connection is automatically closed after the test.
  50. """
  51. import serial
  52. conn = serial.Serial(hardware_port, baudrate=115200, timeout=2)
  53. yield conn
  54. conn.close()
  55. @pytest.fixture(autouse=True)
  56. def fast_test_speed(run_hardware):
  57. """Set speed to 500 for faster integration tests.
  58. This fixture runs automatically for all integration tests.
  59. Restores original speed after the test.
  60. """
  61. if not run_hardware:
  62. yield
  63. return
  64. from modules.core.state import state
  65. original_speed = state.speed
  66. state.speed = 500 # Fast speed for tests
  67. yield
  68. state.speed = original_speed # Restore original speed
  69. @pytest.fixture(autouse=True)
  70. def reset_asyncio_events(run_hardware):
  71. """Reset global asyncio primitives before each test.
  72. The pattern_manager uses global asyncio objects (Lock, Event) that are
  73. bound to the event loop where they were created. When TestClient creates
  74. its own event loop, these become incompatible.
  75. This fixture resets them to None so they get recreated in the current loop.
  76. Also ensures pause/stop state is cleared so tests start fresh.
  77. """
  78. if not run_hardware:
  79. yield
  80. return
  81. import modules.core.pattern_manager as pm
  82. from modules.core.state import state
  83. # Reset pattern_manager's global async primitives
  84. pm.pause_event = None
  85. pm.pattern_lock = None # Will be recreated via get_pattern_lock()
  86. # Reset state's event loop tracking so events get recreated in new loop
  87. state._event_loop = None
  88. state._stop_event = None
  89. state._skip_event = None
  90. # Clear any lingering pause/stop state from previous tests
  91. state._pause_requested = False
  92. state._stop_requested = False
  93. state._skip_requested = False
  94. # Clear playback state
  95. state.current_playing_file = None
  96. state.current_playlist = None
  97. state.playlist_mode = None
  98. state.current_playlist_index = None
  99. yield
  100. # Clean up after test
  101. pm.pause_event = None
  102. pm.pattern_lock = None
  103. state._event_loop = None
  104. state._stop_event = None
  105. state._skip_event = None
  106. state._pause_requested = False
  107. state._stop_requested = False
  108. state._skip_requested = False