first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Tests for dreader-hal."""
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Pytest configuration and fixtures for dreader-hal tests.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
def is_raspberry_pi():
|
||||
"""
|
||||
Check if running on Raspberry Pi hardware.
|
||||
|
||||
Returns:
|
||||
bool: True if running on RPi, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Check for BCM2xxx processor (Raspberry Pi)
|
||||
with open('/proc/cpuinfo', 'r') as f:
|
||||
cpuinfo = f.read()
|
||||
return 'BCM2' in cpuinfo or 'BCM3' in cpuinfo
|
||||
except (FileNotFoundError, PermissionError):
|
||||
return False
|
||||
|
||||
|
||||
# Mock RPi.GPIO at module level if not on Raspberry Pi
|
||||
if not is_raspberry_pi():
|
||||
# Create a mock GPIO module
|
||||
mock_gpio = MagicMock()
|
||||
mock_gpio.BCM = 11
|
||||
mock_gpio.OUT = 0
|
||||
mock_gpio.IN = 1
|
||||
mock_gpio.HIGH = 1
|
||||
mock_gpio.LOW = 0
|
||||
mock_gpio.PUD_UP = 22
|
||||
mock_gpio.FALLING = 32
|
||||
mock_gpio.setmode = MagicMock()
|
||||
mock_gpio.setup = MagicMock()
|
||||
mock_gpio.output = MagicMock()
|
||||
mock_gpio.input = MagicMock(return_value=0)
|
||||
mock_gpio.add_event_detect = MagicMock()
|
||||
mock_gpio.remove_event_detect = MagicMock()
|
||||
mock_gpio.cleanup = MagicMock()
|
||||
|
||||
# Inject mock into sys.modules before any tests import it
|
||||
if 'RPi' not in sys.modules:
|
||||
sys.modules['RPi'] = MagicMock()
|
||||
sys.modules['RPi.GPIO'] = mock_gpio
|
||||
sys.modules['RPi'].GPIO = mock_gpio
|
||||
|
||||
|
||||
# Create pytest markers
|
||||
def pytest_configure(config):
|
||||
"""Register custom pytest markers."""
|
||||
config.addinivalue_line(
|
||||
"markers", "rpi_only: mark test as requiring Raspberry Pi hardware"
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers", "hardware: mark test as requiring physical hardware"
|
||||
)
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
"""
|
||||
Automatically skip tests that require RPi hardware when not on RPi.
|
||||
"""
|
||||
if not is_raspberry_pi():
|
||||
skip_rpi = pytest.mark.skip(reason="Test requires Raspberry Pi hardware")
|
||||
for item in items:
|
||||
if "rpi_only" in item.keywords:
|
||||
item.add_marker(skip_rpi)
|
||||
@@ -0,0 +1 @@
|
||||
"""Unit tests for dreader-hal."""
|
||||
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
Unit tests for dreader_hal.gesture module.
|
||||
|
||||
Tests gesture detection state machine and classification logic.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import time
|
||||
from dreader_hal.gesture import GestureDetector, TouchState
|
||||
from dreader_hal.types import GestureType
|
||||
|
||||
|
||||
class TestGestureDetector:
|
||||
"""Tests for GestureDetector class."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up fresh detector for each test."""
|
||||
self.detector = GestureDetector()
|
||||
|
||||
def test_initial_state(self):
|
||||
"""Test detector initial state."""
|
||||
assert self.detector.state == TouchState.IDLE
|
||||
assert self.detector.start_pos is None
|
||||
assert self.detector.current_pos is None
|
||||
|
||||
def test_tap_detection(self):
|
||||
"""Test tap gesture detection."""
|
||||
# Touch down
|
||||
self.detector.on_touch_down(100, 100)
|
||||
assert self.detector.state == TouchState.TOUCHING
|
||||
|
||||
# Small delay (less than long press)
|
||||
time.sleep(0.1)
|
||||
|
||||
# Touch up at same position
|
||||
gesture = self.detector.on_touch_up(105, 105) # 5px movement
|
||||
|
||||
assert gesture == GestureType.TAP
|
||||
assert self.detector.state == TouchState.IDLE
|
||||
|
||||
def test_long_press_detection(self):
|
||||
"""Test long press gesture detection."""
|
||||
# Touch down
|
||||
self.detector.on_touch_down(100, 100)
|
||||
|
||||
# Wait for long press duration
|
||||
time.sleep(0.6) # Longer than 0.5s threshold
|
||||
|
||||
# Touch up at same position
|
||||
gesture = self.detector.on_touch_up(105, 105)
|
||||
|
||||
assert gesture == GestureType.LONG_PRESS
|
||||
assert self.detector.state == TouchState.IDLE
|
||||
|
||||
def test_swipe_left_detection(self):
|
||||
"""Test swipe left gesture detection."""
|
||||
# Touch down
|
||||
self.detector.on_touch_down(200, 100)
|
||||
|
||||
# Quick movement to the left
|
||||
time.sleep(0.1)
|
||||
gesture = self.detector.on_touch_up(100, 105) # 100px left, 5px down
|
||||
|
||||
assert gesture == GestureType.SWIPE_LEFT
|
||||
|
||||
def test_swipe_right_detection(self):
|
||||
"""Test swipe right gesture detection."""
|
||||
self.detector.on_touch_down(100, 100)
|
||||
time.sleep(0.1)
|
||||
gesture = self.detector.on_touch_up(200, 105) # 100px right
|
||||
|
||||
assert gesture == GestureType.SWIPE_RIGHT
|
||||
|
||||
def test_swipe_up_detection(self):
|
||||
"""Test swipe up gesture detection."""
|
||||
self.detector.on_touch_down(100, 200)
|
||||
time.sleep(0.1)
|
||||
gesture = self.detector.on_touch_up(105, 100) # 100px up
|
||||
|
||||
assert gesture == GestureType.SWIPE_UP
|
||||
|
||||
def test_swipe_down_detection(self):
|
||||
"""Test swipe down gesture detection."""
|
||||
self.detector.on_touch_down(100, 100)
|
||||
time.sleep(0.1)
|
||||
gesture = self.detector.on_touch_up(105, 200) # 100px down
|
||||
|
||||
assert gesture == GestureType.SWIPE_DOWN
|
||||
|
||||
def test_drag_start_detection(self):
|
||||
"""Test drag start detection."""
|
||||
# Touch down
|
||||
self.detector.on_touch_down(100, 100)
|
||||
|
||||
# Move beyond threshold
|
||||
gesture = self.detector.on_touch_move(150, 100) # 50px movement
|
||||
|
||||
assert gesture == GestureType.DRAG_START
|
||||
assert self.detector.state == TouchState.MOVING
|
||||
|
||||
def test_drag_move_detection(self):
|
||||
"""Test drag move events."""
|
||||
# Start dragging
|
||||
self.detector.on_touch_down(100, 100)
|
||||
self.detector.on_touch_move(150, 100) # Start drag
|
||||
|
||||
# Continue moving
|
||||
gesture = self.detector.on_touch_move(200, 100)
|
||||
|
||||
assert gesture == GestureType.DRAG_MOVE
|
||||
assert self.detector.state == TouchState.MOVING
|
||||
|
||||
def test_drag_end_detection(self):
|
||||
"""Test drag end."""
|
||||
# Start dragging
|
||||
self.detector.on_touch_down(100, 100)
|
||||
self.detector.on_touch_move(150, 100)
|
||||
|
||||
# Touch up
|
||||
time.sleep(0.6) # Wait long enough to not be a swipe
|
||||
gesture = self.detector.on_touch_up(200, 100)
|
||||
|
||||
assert gesture == GestureType.DRAG_END
|
||||
|
||||
def test_reset(self):
|
||||
"""Test detector reset."""
|
||||
# Set some state
|
||||
self.detector.on_touch_down(100, 100)
|
||||
assert self.detector.state == TouchState.TOUCHING
|
||||
|
||||
# Reset
|
||||
self.detector.reset()
|
||||
|
||||
# Check clean state
|
||||
assert self.detector.state == TouchState.IDLE
|
||||
assert self.detector.start_pos is None
|
||||
assert self.detector.current_pos is None
|
||||
|
||||
def test_custom_thresholds(self):
|
||||
"""Test creating detector with custom thresholds."""
|
||||
detector = GestureDetector(
|
||||
tap_threshold=50.0,
|
||||
long_press_duration=1.0
|
||||
)
|
||||
|
||||
assert detector.tap_threshold == 50.0
|
||||
assert detector.long_press_duration == 1.0
|
||||
|
||||
def test_pinch_detection(self):
|
||||
"""Test two-finger pinch detection."""
|
||||
# First finger down
|
||||
self.detector.on_touch_down(100, 100, finger=0)
|
||||
|
||||
# Second finger down
|
||||
self.detector.on_touch_down(200, 100, finger=1)
|
||||
|
||||
# Move fingers apart (pinch out)
|
||||
self.detector.on_touch_move(90, 100, finger=0)
|
||||
self.detector.on_touch_move(210, 100, finger=1)
|
||||
|
||||
# Second finger up
|
||||
gesture = self.detector.on_touch_up(210, 100, finger=1)
|
||||
|
||||
assert gesture == GestureType.PINCH_OUT
|
||||
|
||||
def test_check_long_press(self):
|
||||
"""Test long press check during touch."""
|
||||
# Touch down
|
||||
self.detector.on_touch_down(100, 100)
|
||||
|
||||
# Check before threshold
|
||||
time.sleep(0.2)
|
||||
assert not self.detector.check_long_press()
|
||||
|
||||
# Check after threshold
|
||||
time.sleep(0.4) # Total 0.6s
|
||||
assert self.detector.check_long_press()
|
||||
assert self.detector.state == TouchState.LONG_PRESS_DETECTED
|
||||
|
||||
def test_no_gesture_on_small_movement(self):
|
||||
"""Test that small movements during long delay don't trigger swipe."""
|
||||
self.detector.on_touch_down(100, 100)
|
||||
time.sleep(0.6) # Wait long
|
||||
|
||||
# Small movement
|
||||
gesture = self.detector.on_touch_up(110, 110) # Only 14px diagonal
|
||||
|
||||
# Should be long press, not swipe
|
||||
assert gesture == GestureType.LONG_PRESS
|
||||
|
||||
|
||||
class TestTouchState:
|
||||
"""Tests for TouchState enum."""
|
||||
|
||||
def test_states_defined(self):
|
||||
"""Test all required states are defined."""
|
||||
assert TouchState.IDLE
|
||||
assert TouchState.TOUCHING
|
||||
assert TouchState.MOVING
|
||||
assert TouchState.LONG_PRESS_DETECTED
|
||||
|
||||
def test_state_values(self):
|
||||
"""Test state enum values."""
|
||||
assert TouchState.IDLE.value == "idle"
|
||||
assert TouchState.TOUCHING.value == "touching"
|
||||
assert TouchState.MOVING.value == "moving"
|
||||
assert TouchState.LONG_PRESS_DETECTED.value == "long_press_detected"
|
||||
@@ -0,0 +1,327 @@
|
||||
"""
|
||||
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
|
||||
@@ -0,0 +1,206 @@
|
||||
"""
|
||||
Unit tests for dreader_hal.types module.
|
||||
|
||||
Tests type definitions, enums, and data structures.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from dreader_hal.types import (
|
||||
GestureType,
|
||||
TouchEvent,
|
||||
PowerStats,
|
||||
Orientation,
|
||||
RefreshMode,
|
||||
GESTURE_THRESHOLDS,
|
||||
)
|
||||
|
||||
|
||||
class TestGestureType:
|
||||
"""Tests for GestureType enum."""
|
||||
|
||||
def test_all_gestures_defined(self):
|
||||
"""Test that all required gestures are defined."""
|
||||
required_gestures = [
|
||||
"TAP", "LONG_PRESS",
|
||||
"SWIPE_LEFT", "SWIPE_RIGHT", "SWIPE_UP", "SWIPE_DOWN",
|
||||
"PINCH_IN", "PINCH_OUT",
|
||||
"DRAG_START", "DRAG_MOVE", "DRAG_END"
|
||||
]
|
||||
|
||||
for gesture_name in required_gestures:
|
||||
assert hasattr(GestureType, gesture_name), f"Missing gesture: {gesture_name}"
|
||||
|
||||
def test_gesture_values(self):
|
||||
"""Test gesture enum values."""
|
||||
assert GestureType.TAP.value == "tap"
|
||||
assert GestureType.SWIPE_LEFT.value == "swipe_left"
|
||||
assert GestureType.PINCH_OUT.value == "pinch_out"
|
||||
|
||||
|
||||
class TestTouchEvent:
|
||||
"""Tests for TouchEvent dataclass."""
|
||||
|
||||
def test_create_touch_event(self):
|
||||
"""Test creating a TouchEvent."""
|
||||
event = TouchEvent(
|
||||
gesture=GestureType.TAP,
|
||||
x=100,
|
||||
y=200,
|
||||
timestamp_ms=1234567890.0
|
||||
)
|
||||
|
||||
assert event.gesture == GestureType.TAP
|
||||
assert event.x == 100
|
||||
assert event.y == 200
|
||||
assert event.x2 is None
|
||||
assert event.y2 is None
|
||||
assert event.timestamp_ms == 1234567890.0
|
||||
|
||||
def test_touch_event_with_two_fingers(self):
|
||||
"""Test TouchEvent with two-finger coordinates."""
|
||||
event = TouchEvent(
|
||||
gesture=GestureType.PINCH_OUT,
|
||||
x=100,
|
||||
y=200,
|
||||
x2=300,
|
||||
y2=400,
|
||||
)
|
||||
|
||||
assert event.x2 == 300
|
||||
assert event.y2 == 400
|
||||
|
||||
def test_touch_event_defaults(self):
|
||||
"""Test TouchEvent default values."""
|
||||
event = TouchEvent(gesture=GestureType.TAP, x=0, y=0)
|
||||
|
||||
assert event.x2 is None
|
||||
assert event.y2 is None
|
||||
assert event.timestamp_ms == 0
|
||||
|
||||
|
||||
class TestPowerStats:
|
||||
"""Tests for PowerStats dataclass."""
|
||||
|
||||
def test_create_power_stats(self):
|
||||
"""Test creating PowerStats."""
|
||||
stats = PowerStats(
|
||||
voltage=3.7,
|
||||
current=150.0,
|
||||
power=555.0,
|
||||
battery_percent=85.0,
|
||||
time_remaining=180,
|
||||
is_charging=False
|
||||
)
|
||||
|
||||
assert stats.voltage == 3.7
|
||||
assert stats.current == 150.0
|
||||
assert stats.power == 555.0
|
||||
assert stats.battery_percent == 85.0
|
||||
assert stats.time_remaining == 180
|
||||
assert stats.is_charging is False
|
||||
|
||||
def test_charging_state(self):
|
||||
"""Test PowerStats with charging enabled."""
|
||||
stats = PowerStats(
|
||||
voltage=4.2,
|
||||
current=-100.0, # Negative = charging
|
||||
power=420.0,
|
||||
battery_percent=95.0,
|
||||
time_remaining=None, # N/A when charging
|
||||
is_charging=True
|
||||
)
|
||||
|
||||
assert stats.is_charging is True
|
||||
assert stats.time_remaining is None
|
||||
|
||||
|
||||
class TestOrientation:
|
||||
"""Tests for Orientation enum."""
|
||||
|
||||
def test_orientation_angles(self):
|
||||
"""Test orientation angle values."""
|
||||
assert Orientation.PORTRAIT_0.value == 0
|
||||
assert Orientation.LANDSCAPE_90.value == 90
|
||||
assert Orientation.PORTRAIT_180.value == 180
|
||||
assert Orientation.LANDSCAPE_270.value == 270
|
||||
|
||||
def test_is_portrait(self):
|
||||
"""Test is_portrait property."""
|
||||
assert Orientation.PORTRAIT_0.is_portrait is True
|
||||
assert Orientation.PORTRAIT_180.is_portrait is True
|
||||
assert Orientation.LANDSCAPE_90.is_portrait is False
|
||||
assert Orientation.LANDSCAPE_270.is_portrait is False
|
||||
|
||||
def test_is_landscape(self):
|
||||
"""Test is_landscape property."""
|
||||
assert Orientation.LANDSCAPE_90.is_landscape is True
|
||||
assert Orientation.LANDSCAPE_270.is_landscape is True
|
||||
assert Orientation.PORTRAIT_0.is_landscape is False
|
||||
assert Orientation.PORTRAIT_180.is_landscape is False
|
||||
|
||||
def test_angle_property(self):
|
||||
"""Test angle property."""
|
||||
assert Orientation.PORTRAIT_0.angle == 0
|
||||
assert Orientation.LANDSCAPE_90.angle == 90
|
||||
assert Orientation.PORTRAIT_180.angle == 180
|
||||
assert Orientation.LANDSCAPE_270.angle == 270
|
||||
|
||||
|
||||
class TestRefreshMode:
|
||||
"""Tests for RefreshMode enum."""
|
||||
|
||||
def test_refresh_modes_defined(self):
|
||||
"""Test that all refresh modes are defined."""
|
||||
assert RefreshMode.AUTO
|
||||
assert RefreshMode.FAST
|
||||
assert RefreshMode.QUALITY
|
||||
assert RefreshMode.FULL
|
||||
|
||||
def test_refresh_mode_values(self):
|
||||
"""Test refresh mode values."""
|
||||
assert RefreshMode.AUTO.value == "auto"
|
||||
assert RefreshMode.FAST.value == "fast"
|
||||
assert RefreshMode.QUALITY.value == "quality"
|
||||
assert RefreshMode.FULL.value == "full"
|
||||
|
||||
|
||||
class TestGestureThresholds:
|
||||
"""Tests for GESTURE_THRESHOLDS configuration."""
|
||||
|
||||
def test_thresholds_exist(self):
|
||||
"""Test that all required thresholds are defined."""
|
||||
required_thresholds = [
|
||||
'tap_distance',
|
||||
'swipe_min_distance',
|
||||
'drag_threshold',
|
||||
'long_press_duration',
|
||||
'tap_max_duration',
|
||||
'swipe_max_duration',
|
||||
'swipe_angle_threshold',
|
||||
]
|
||||
|
||||
for threshold in required_thresholds:
|
||||
assert threshold in GESTURE_THRESHOLDS, f"Missing threshold: {threshold}"
|
||||
|
||||
def test_threshold_values_reasonable(self):
|
||||
"""Test that threshold values are reasonable."""
|
||||
# Distance thresholds should be positive
|
||||
assert GESTURE_THRESHOLDS['tap_distance'] > 0
|
||||
assert GESTURE_THRESHOLDS['swipe_min_distance'] > 0
|
||||
assert GESTURE_THRESHOLDS['drag_threshold'] > 0
|
||||
|
||||
# Duration thresholds should be positive
|
||||
assert GESTURE_THRESHOLDS['long_press_duration'] > 0
|
||||
assert GESTURE_THRESHOLDS['tap_max_duration'] > 0
|
||||
assert GESTURE_THRESHOLDS['swipe_max_duration'] > 0
|
||||
|
||||
# Angle threshold should be 0-90 degrees
|
||||
assert 0 <= GESTURE_THRESHOLDS['swipe_angle_threshold'] <= 90
|
||||
|
||||
def test_threshold_relationships(self):
|
||||
"""Test logical relationships between thresholds."""
|
||||
# Long press should be longer than tap
|
||||
assert GESTURE_THRESHOLDS['long_press_duration'] > GESTURE_THRESHOLDS['tap_max_duration']
|
||||
|
||||
# Swipe should have minimum distance
|
||||
assert GESTURE_THRESHOLDS['swipe_min_distance'] >= GESTURE_THRESHOLDS['tap_distance']
|
||||
Reference in New Issue
Block a user