dreader-hal/tests/unit/test_hal.py
Duncan Tourolle be3aed6e5e
Some checks failed
Python CI / test (3.12) (push) Failing after 41s
Python CI / test (3.13) (push) Failing after 39s
mocking in tests
2025-11-10 18:23:19 +01:00

345 lines
12 KiB
Python

"""
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