""" Unit tests for dreader_hal.hal and dreader_hal.ereader_hal modules. Tests the DisplayHAL interface and EReaderDisplayHAL implementation. """ import pytest import sys from unittest.mock import AsyncMock, MagicMock, patch, Mock from PIL import Image from dreader_hal.hal import DisplayHAL from dreader_hal.types import GestureType, TouchEvent # Mock the hardware driver modules before importing ereader_hal # This prevents import errors when the hardware modules can't be loaded sys.modules['dreader_hal.display.it8951'] = Mock() sys.modules['dreader_hal.touch.ft5xx6'] = Mock() sys.modules['dreader_hal.sensors.bma400'] = Mock() sys.modules['dreader_hal.rtc.pcf8523'] = Mock() sys.modules['dreader_hal.power.ina219'] = Mock() from dreader_hal.ereader_hal import EReaderDisplayHAL class TestDisplayHALInterface: """Tests for DisplayHAL abstract interface.""" def test_displayhal_is_abstract(self): """Test that DisplayHAL cannot be instantiated directly.""" with pytest.raises(TypeError): DisplayHAL() def test_required_methods_defined(self): """Test that required methods are defined in interface.""" required_methods = [ 'show_image', 'get_touch_event', 'set_brightness', ] for method_name in required_methods: assert hasattr(DisplayHAL, method_name) class TestEReaderDisplayHAL: """Tests for EReaderDisplayHAL implementation.""" @pytest.fixture def mock_components(self): """Create mock hardware components.""" # Create mock driver classes mock_display_class = MagicMock() mock_touch_class = MagicMock() mock_orientation_class = MagicMock() mock_rtc_class = MagicMock() mock_power_class = MagicMock() # Set up the mocks to return mock instances with async methods mock_display_instance = MagicMock() mock_display_instance.initialize = AsyncMock() mock_display_instance.cleanup = AsyncMock() mock_display_instance.show_image = AsyncMock() mock_display_instance.set_brightness = AsyncMock() mock_display_instance.sleep = AsyncMock() mock_display_instance.wake = AsyncMock() mock_display_instance.refresh_count = 0 mock_display_class.return_value = mock_display_instance mock_touch_instance = MagicMock() mock_touch_instance.initialize = AsyncMock() mock_touch_instance.cleanup = AsyncMock() mock_touch_instance.get_touch_event = AsyncMock(return_value=None) mock_touch_instance.set_polling_rate = AsyncMock() mock_touch_class.return_value = mock_touch_instance mock_orientation_instance = MagicMock() mock_orientation_instance.initialize = AsyncMock() mock_orientation_instance.cleanup = AsyncMock() mock_orientation_instance.current_angle = 0 mock_orientation_class.return_value = mock_orientation_instance mock_rtc_instance = MagicMock() mock_rtc_instance.initialize = AsyncMock() mock_rtc_instance.cleanup = AsyncMock() mock_rtc_class.return_value = mock_rtc_instance mock_power_instance = MagicMock() mock_power_instance.initialize = AsyncMock() mock_power_instance.cleanup = AsyncMock() mock_power_instance.get_battery_percent = AsyncMock(return_value=75.0) mock_power_instance.is_low_battery = AsyncMock(return_value=False) mock_power_class.return_value = mock_power_instance # Inject the mock classes into the sys.modules mocks sys.modules['dreader_hal.display.it8951'].IT8951DisplayDriver = mock_display_class sys.modules['dreader_hal.touch.ft5xx6'].FT5xx6TouchDriver = mock_touch_class sys.modules['dreader_hal.sensors.bma400'].BMA400OrientationSensor = mock_orientation_class sys.modules['dreader_hal.rtc.pcf8523'].PCF8523RTC = mock_rtc_class sys.modules['dreader_hal.power.ina219'].INA219PowerMonitor = mock_power_class yield { 'display': mock_display_instance, 'touch': mock_touch_instance, 'orientation': mock_orientation_instance, 'rtc': mock_rtc_instance, 'power': mock_power_instance, } def test_hal_initialization(self, mock_components): """Test HAL initialization with default parameters.""" hal = EReaderDisplayHAL(virtual_display=True) assert hal.width == 800 assert hal.height == 1200 assert hal._brightness == 5 assert hal._initialized is False def test_hal_custom_dimensions(self, mock_components): """Test HAL with custom dimensions.""" hal = EReaderDisplayHAL( width=1024, height=768, virtual_display=True ) assert hal.width == 1024 assert hal.height == 768 @pytest.mark.asyncio async def test_initialize(self, mock_components): """Test HAL initialize method.""" # Setup mocks for component in mock_components.values(): component.initialize = AsyncMock() hal = EReaderDisplayHAL(virtual_display=True) # Initialize await hal.initialize() # Verify all components initialized assert hal._initialized is True mock_components['display'].initialize.assert_called_once() mock_components['touch'].initialize.assert_called_once() @pytest.mark.asyncio async def test_initialize_idempotent(self, mock_components): """Test that initialize can be called multiple times safely.""" for component in mock_components.values(): component.initialize = AsyncMock() hal = EReaderDisplayHAL(virtual_display=True) # Initialize twice await hal.initialize() await hal.initialize() # Should only initialize once assert mock_components['display'].initialize.call_count == 1 @pytest.mark.asyncio async def test_cleanup(self, mock_components): """Test HAL cleanup method.""" for component in mock_components.values(): component.initialize = AsyncMock() component.cleanup = AsyncMock() hal = EReaderDisplayHAL(virtual_display=True) await hal.initialize() # Cleanup await hal.cleanup() # Verify all components cleaned up assert hal._initialized is False mock_components['display'].cleanup.assert_called_once() mock_components['touch'].cleanup.assert_called_once() @pytest.mark.asyncio async def test_show_image(self, mock_components): """Test show_image method.""" mock_components['display'].initialize = AsyncMock() mock_components['display'].show_image = AsyncMock() mock_components['touch'].initialize = AsyncMock() hal = EReaderDisplayHAL(virtual_display=True, enable_orientation=False) await hal.initialize() # Create test image image = Image.new('RGB', (800, 1200), color=(255, 255, 255)) # Show image await hal.show_image(image) # Verify display driver called mock_components['display'].show_image.assert_called_once() @pytest.mark.asyncio async def test_show_image_not_initialized(self, mock_components): """Test show_image raises error when not initialized.""" hal = EReaderDisplayHAL(virtual_display=True) image = Image.new('RGB', (800, 1200)) with pytest.raises(RuntimeError, match="not initialized"): await hal.show_image(image) @pytest.mark.asyncio async def test_get_touch_event(self, mock_components): """Test get_touch_event method.""" mock_components['display'].initialize = AsyncMock() mock_components['touch'].initialize = AsyncMock() mock_components['touch'].get_touch_event = AsyncMock( return_value=TouchEvent(GestureType.TAP, 100, 200) ) hal = EReaderDisplayHAL(virtual_display=True) await hal.initialize() # Get touch event event = await hal.get_touch_event() # Verify assert event is not None assert event.gesture == GestureType.TAP assert event.x == 100 assert event.y == 200 @pytest.mark.asyncio async def test_set_brightness(self, mock_components): """Test set_brightness method.""" mock_components['display'].initialize = AsyncMock() mock_components['display'].set_brightness = AsyncMock() mock_components['touch'].initialize = AsyncMock() hal = EReaderDisplayHAL(virtual_display=True) await hal.initialize() # Set brightness await hal.set_brightness(7) # Verify assert hal._brightness == 7 mock_components['display'].set_brightness.assert_called_once_with(7) @pytest.mark.asyncio async def test_set_brightness_invalid(self, mock_components): """Test set_brightness with invalid values.""" hal = EReaderDisplayHAL(virtual_display=True) with pytest.raises(ValueError, match="must be 0-10"): await hal.set_brightness(11) with pytest.raises(ValueError, match="must be 0-10"): await hal.set_brightness(-1) @pytest.mark.asyncio async def test_get_battery_level(self, mock_components): """Test get_battery_level method.""" mock_components['display'].initialize = AsyncMock() mock_components['touch'].initialize = AsyncMock() mock_components['power'].initialize = AsyncMock() mock_components['power'].get_battery_percent = AsyncMock(return_value=85.0) hal = EReaderDisplayHAL(virtual_display=True, enable_power_monitor=True) await hal.initialize() # Get battery level level = await hal.get_battery_level() assert level == 85.0 mock_components['power'].get_battery_percent.assert_called_once() @pytest.mark.asyncio async def test_get_battery_level_no_power_monitor(self, mock_components): """Test get_battery_level when power monitor disabled.""" hal = EReaderDisplayHAL(virtual_display=True, enable_power_monitor=False) # Should return 0.0 when power monitor not enabled level = await hal.get_battery_level() assert level == 0.0 @pytest.mark.asyncio async def test_is_low_battery(self, mock_components): """Test is_low_battery method.""" mock_components['display'].initialize = AsyncMock() mock_components['touch'].initialize = AsyncMock() mock_components['power'].initialize = AsyncMock() mock_components['power'].is_low_battery = AsyncMock(return_value=True) hal = EReaderDisplayHAL(virtual_display=True, enable_power_monitor=True) await hal.initialize() # Check low battery is_low = await hal.is_low_battery(20.0) assert is_low is True mock_components['power'].is_low_battery.assert_called_once_with(20.0) def test_disable_optional_components(self, mock_components): """Test HAL with optional components disabled.""" hal = EReaderDisplayHAL( virtual_display=True, enable_orientation=False, enable_rtc=False, enable_power_monitor=False, ) assert hal.orientation is None assert hal.rtc is None assert hal.power is None @pytest.mark.asyncio async def test_set_low_power_mode(self, mock_components): """Test set_low_power_mode method.""" mock_components['display'].initialize = AsyncMock() mock_components['display'].sleep = AsyncMock() mock_components['display'].wake = AsyncMock() mock_components['touch'].initialize = AsyncMock() mock_components['touch'].set_polling_rate = AsyncMock() hal = EReaderDisplayHAL(virtual_display=True) await hal.initialize() # Enable low power mode await hal.set_low_power_mode(True) mock_components['display'].sleep.assert_called_once() mock_components['touch'].set_polling_rate.assert_called_once_with(10) # Disable low power mode await hal.set_low_power_mode(False) mock_components['display'].wake.assert_called_once() assert mock_components['touch'].set_polling_rate.call_count == 2 def test_refresh_count_property(self, mock_components): """Test refresh_count property.""" mock_components['display'].refresh_count = 42 hal = EReaderDisplayHAL(virtual_display=True) assert hal.refresh_count == 42