328 lines
12 KiB
Python
328 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
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from PIL import Image
|
|
|
|
from dreader_hal.hal import DisplayHAL
|
|
from dreader_hal.ereader_hal import EReaderDisplayHAL
|
|
from dreader_hal.types import GestureType, TouchEvent
|
|
|
|
|
|
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."""
|
|
# Since imports are now lazy (inside __init__), we need to patch the actual modules
|
|
with patch('dreader_hal.display.it8951.IT8951DisplayDriver') as mock_display, \
|
|
patch('dreader_hal.touch.ft5xx6.FT5xx6TouchDriver') as mock_touch, \
|
|
patch('dreader_hal.sensors.bma400.BMA400OrientationSensor') as mock_orientation, \
|
|
patch('dreader_hal.rtc.pcf8523.PCF8523RTC') as mock_rtc, \
|
|
patch('dreader_hal.power.ina219.INA219PowerMonitor') as mock_power:
|
|
|
|
# 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.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.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.return_value = mock_orientation_instance
|
|
|
|
mock_rtc_instance = MagicMock()
|
|
mock_rtc_instance.initialize = AsyncMock()
|
|
mock_rtc_instance.cleanup = AsyncMock()
|
|
mock_rtc.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.return_value = mock_power_instance
|
|
|
|
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
|