Update tests

This commit is contained in:
Thomas Farstrike
2025-12-19 11:13:40 +01:00
parent a31ac2f112
commit 08d1b28691
5 changed files with 376 additions and 163 deletions
@@ -24,6 +24,7 @@ from .mocks import (
MockI2S,
MockTimer,
MockSocket,
MockNeoPixel,
# MPOS mocks
MockTaskManager,
@@ -58,6 +59,7 @@ __all__ = [
'MockI2S',
'MockTimer',
'MockSocket',
'MockNeoPixel',
# MPOS mocks
'MockTaskManager',
@@ -204,6 +204,50 @@ class MockTimer:
cls._all_timers.clear()
class MockNeoPixel:
"""Mock neopixel.NeoPixel for testing LED operations."""
def __init__(self, pin, num_leds, bpp=3, timing=1):
self.pin = pin
self.num_leds = num_leds
self.bpp = bpp
self.timing = timing
self.pixels = [(0, 0, 0)] * num_leds
self.write_count = 0
def __setitem__(self, index, value):
"""Set LED color (R, G, B) or (R, G, B, W) tuple."""
if 0 <= index < self.num_leds:
self.pixels[index] = value
def __getitem__(self, index):
"""Get LED color."""
if 0 <= index < self.num_leds:
return self.pixels[index]
return (0, 0, 0)
def __len__(self):
"""Return number of LEDs."""
return self.num_leds
def fill(self, color):
"""Fill all LEDs with the same color."""
for i in range(self.num_leds):
self.pixels[i] = color
def write(self):
"""Update hardware (mock - just increment counter)."""
self.write_count += 1
def get_all_colors(self):
"""Get all LED colors (for testing assertions)."""
return self.pixels.copy()
def reset_write_count(self):
"""Reset the write counter (for testing)."""
self.write_count = 0
class MockMachine:
"""
Mock machine module containing all hardware mocks.
+300
View File
@@ -0,0 +1,300 @@
# MicroPythonOS Testing Guide
This directory contains the test suite for MicroPythonOS. Tests can run on both desktop (for fast iteration) and on-device (for hardware verification).
## Quick Start
```bash
# Run all tests
./tests/unittest.sh
# Run a specific test
./tests/unittest.sh tests/test_graphical_keyboard_q_button_bug.py
# Run on device
./tests/unittest.sh tests/test_graphical_keyboard_q_button_bug.py --ondevice
```
## Test Architecture
### Directory Structure
```
tests/
├── base/ # Base test classes (DRY patterns)
│ ├── __init__.py # Exports GraphicalTestBase, KeyboardTestBase
│ ├── graphical_test_base.py
│ └── keyboard_test_base.py
├── screenshots/ # Captured screenshots for visual regression
├── test_*.py # Test files
├── unittest.sh # Test runner script
└── README.md # This file
```
### Testing Modules
MicroPythonOS provides two testing modules:
1. **`mpos.testing`** - Hardware and system mocks
- Location: `internal_filesystem/lib/mpos/testing/`
- Use for: Mocking hardware (Pin, PWM, I2S, NeoPixel), network, async operations
2. **`mpos.ui.testing`** - LVGL/UI testing utilities
- Location: `internal_filesystem/lib/mpos/ui/testing.py`
- Use for: UI interaction, screenshots, widget inspection
## Base Test Classes
### GraphicalTestBase
Base class for all graphical (LVGL) tests. Provides:
- Automatic screen creation/cleanup
- Screenshot capture
- Widget finding utilities
- Custom assertions
```python
from base import GraphicalTestBase
class TestMyUI(GraphicalTestBase):
def test_something(self):
# self.screen is already created
label = lv.label(self.screen)
label.set_text("Hello")
self.wait_for_render()
self.assertTextPresent("Hello")
self.capture_screenshot("my_test.raw")
```
**Key Methods:**
- `wait_for_render(iterations=5)` - Process LVGL tasks
- `capture_screenshot(filename)` - Save screenshot
- `find_label_with_text(text)` - Find label widget
- `click_button(button)` - Simulate button click
- `assertTextPresent(text)` - Assert text is on screen
- `assertWidgetVisible(widget)` - Assert widget is visible
### KeyboardTestBase
Extends GraphicalTestBase for keyboard tests. Provides:
- Keyboard and textarea creation
- Reliable keyboard button clicking
- Textarea assertions
```python
from base import KeyboardTestBase
class TestMyKeyboard(KeyboardTestBase):
def test_typing(self):
self.create_keyboard_scene()
self.click_keyboard_button("h")
self.click_keyboard_button("i")
self.assertTextareaText("hi")
```
**Key Methods:**
- `create_keyboard_scene()` - Create textarea + MposKeyboard
- `click_keyboard_button(text)` - Click keyboard button reliably
- `type_text(text)` - Type a string
- `get_textarea_text()` - Get textarea content
- `clear_textarea()` - Clear textarea
- `assertTextareaText(expected)` - Assert textarea content
- `assertTextareaEmpty()` - Assert textarea is empty
## Mock Classes
Import mocks from `mpos.testing`:
```python
from mpos.testing import (
# Hardware mocks
MockMachine, # Full machine module mock
MockPin, # GPIO pins
MockPWM, # PWM for buzzer
MockI2S, # Audio I2S
MockTimer, # Hardware timers
MockNeoPixel, # LED strips
MockSocket, # Network sockets
# MPOS mocks
MockTaskManager, # Async task management
MockDownloadManager, # HTTP downloads
# Network mocks
MockNetwork, # WiFi/network module
MockRequests, # HTTP requests
MockResponse, # HTTP responses
# Utility mocks
MockTime, # Time functions
MockJSON, # JSON parsing
# Helpers
inject_mocks, # Inject mocks into sys.modules
create_mock_module, # Create mock module
)
```
### Injecting Mocks
```python
from mpos.testing import inject_mocks, MockMachine, MockNetwork
# Inject before importing modules that use hardware
inject_mocks({
'machine': MockMachine(),
'network': MockNetwork(connected=True),
})
# Now import the module under test
from mpos.hardware import some_module
```
### Mock Examples
**MockNeoPixel:**
```python
from mpos.testing import MockNeoPixel, MockPin
pin = MockPin(5)
leds = MockNeoPixel(pin, 10)
leds[0] = (255, 0, 0) # Set first LED to red
leds.write()
assert leds.write_count == 1
assert leds[0] == (255, 0, 0)
```
**MockRequests:**
```python
from mpos.testing import MockRequests
mock_requests = MockRequests()
mock_requests.set_next_response(
status_code=200,
text='{"status": "ok"}',
headers={'Content-Type': 'application/json'}
)
response = mock_requests.get("https://api.example.com/data")
assert response.status_code == 200
```
**MockTimer:**
```python
from mpos.testing import MockTimer
timer = MockTimer(0)
timer.init(period=1000, mode=MockTimer.PERIODIC, callback=my_callback)
# Manually trigger for testing
timer.trigger()
# Or trigger all timers
MockTimer.trigger_all()
```
## Test Naming Conventions
- `test_*.py` - Standard unit tests
- `test_graphical_*.py` - Tests requiring LVGL/UI (detected by unittest.sh)
- `manual_test_*.py` - Manual tests (not run automatically)
## Writing New Tests
### Simple Unit Test
```python
import unittest
class TestMyFeature(unittest.TestCase):
def test_something(self):
result = my_function()
self.assertEqual(result, expected)
```
### Graphical Test
```python
from base import GraphicalTestBase
import lvgl as lv
class TestMyUI(GraphicalTestBase):
def test_button_click(self):
button = lv.button(self.screen)
label = lv.label(button)
label.set_text("Click Me")
self.wait_for_render()
self.click_button(button)
# Verify result
```
### Keyboard Test
```python
from base import KeyboardTestBase
class TestMyKeyboard(KeyboardTestBase):
def test_input(self):
self.create_keyboard_scene()
self.type_text("hello")
self.assertTextareaText("hello")
self.click_keyboard_button("Enter")
```
### Test with Mocks
```python
import unittest
from mpos.testing import MockNetwork, inject_mocks
class TestNetworkFeature(unittest.TestCase):
def setUp(self):
self.mock_network = MockNetwork(connected=True)
inject_mocks({'network': self.mock_network})
def test_connected(self):
from my_module import check_connection
self.assertTrue(check_connection())
def test_disconnected(self):
self.mock_network.set_connected(False)
from my_module import check_connection
self.assertFalse(check_connection())
```
## Best Practices
1. **Use base classes** - Extend `GraphicalTestBase` or `KeyboardTestBase` for UI tests
2. **Use mpos.testing mocks** - Don't create inline mocks; use the centralized ones
3. **Clean up in tearDown** - Base classes handle this, but custom tests should clean up
4. **Don't include `if __name__ == '__main__'`** - The test runner handles this
5. **Use descriptive test names** - `test_keyboard_q_button_works` not `test_1`
6. **Add docstrings** - Explain what the test verifies and why
## Debugging Tests
```bash
# Run with verbose output
./tests/unittest.sh tests/test_my_test.py
# Run with GDB (desktop only)
gdb --args ./lvgl_micropython/build/lvgl_micropy_unix -X heapsize=8M tests/test_my_test.py
```
## Screenshots
Screenshots are saved to `tests/screenshots/` in raw format. Convert to PNG:
```bash
cd tests/screenshots
./convert_to_png.sh
```
-102
View File
@@ -1,102 +0,0 @@
# Hardware Mocks for Testing AudioFlinger and LightsManager
# Provides mock implementations of PWM, I2S, NeoPixel, and Pin classes
class MockPin:
"""Mock machine.Pin for testing."""
IN = 0
OUT = 1
PULL_UP = 2
def __init__(self, pin_number, mode=None, pull=None):
self.pin_number = pin_number
self.mode = mode
self.pull = pull
self._value = 0
def value(self, val=None):
if val is not None:
self._value = val
return self._value
class MockPWM:
"""Mock machine.PWM for testing buzzer."""
def __init__(self, pin, freq=0, duty=0):
self.pin = pin
self.last_freq = freq
self.last_duty = duty
self.freq_history = []
self.duty_history = []
def freq(self, value=None):
"""Set or get frequency."""
if value is not None:
self.last_freq = value
self.freq_history.append(value)
return self.last_freq
def duty_u16(self, value=None):
"""Set or get duty cycle (0-65535)."""
if value is not None:
self.last_duty = value
self.duty_history.append(value)
return self.last_duty
class MockI2S:
"""Mock machine.I2S for testing audio playback."""
TX = 0
MONO = 1
STEREO = 2
def __init__(self, id, sck, ws, sd, mode, bits, format, rate, ibuf):
self.id = id
self.sck = sck
self.ws = ws
self.sd = sd
self.mode = mode
self.bits = bits
self.format = format
self.rate = rate
self.ibuf = ibuf
self.written_bytes = []
self.total_bytes_written = 0
def write(self, buf):
"""Simulate writing to I2S hardware."""
self.written_bytes.append(bytes(buf))
self.total_bytes_written += len(buf)
return len(buf)
def deinit(self):
"""Deinitialize I2S."""
pass
class MockNeoPixel:
"""Mock neopixel.NeoPixel for testing LEDs."""
def __init__(self, pin, num_leds):
self.pin = pin
self.num_leds = num_leds
self.pixels = [(0, 0, 0)] * num_leds
self.write_count = 0
def __setitem__(self, index, value):
"""Set LED color (R, G, B) tuple."""
if 0 <= index < self.num_leds:
self.pixels[index] = value
def __getitem__(self, index):
"""Get LED color."""
if 0 <= index < self.num_leds:
return self.pixels[index]
return (0, 0, 0)
def write(self):
"""Update hardware (mock - just increment counter)."""
self.write_count += 1
+30 -61
View File
@@ -13,32 +13,12 @@ import unittest
import lvgl as lv
import time
import mpos.ui.anim
from mpos.ui.keyboard import MposKeyboard
from mpos.ui.testing import wait_for_render
from base import KeyboardTestBase
class TestKeyboardAnimation(unittest.TestCase):
class TestKeyboardAnimation(KeyboardTestBase):
"""Test MposKeyboard compatibility with animation system."""
def setUp(self):
"""Set up test fixtures."""
# Create a test screen
self.screen = lv.obj()
self.screen.set_size(320, 240)
lv.screen_load(self.screen)
# Create textarea
self.textarea = lv.textarea(self.screen)
self.textarea.set_size(280, 40)
self.textarea.align(lv.ALIGN.TOP_MID, 0, 10)
self.textarea.set_one_line(True)
print("\n=== Animation Test Setup Complete ===")
def tearDown(self):
"""Clean up after test."""
lv.screen_load(lv.obj())
print("=== Test Cleanup Complete ===\n")
def test_keyboard_has_set_style_opa(self):
"""
Test that MposKeyboard has set_style_opa method.
@@ -47,24 +27,22 @@ class TestKeyboardAnimation(unittest.TestCase):
"""
print("Testing that MposKeyboard has set_style_opa...")
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
keyboard.add_flag(lv.obj.FLAG.HIDDEN)
self.create_keyboard_scene()
self.keyboard.add_flag(lv.obj.FLAG.HIDDEN)
# Verify method exists
self.assertTrue(
hasattr(keyboard, 'set_style_opa'),
hasattr(self.keyboard, 'set_style_opa'),
"MposKeyboard missing set_style_opa method"
)
self.assertTrue(
callable(getattr(keyboard, 'set_style_opa')),
callable(getattr(self.keyboard, 'set_style_opa')),
"MposKeyboard.set_style_opa is not callable"
)
# Try calling it (should not raise AttributeError)
try:
keyboard.set_style_opa(128, 0)
self.keyboard.set_style_opa(128, 0)
print("set_style_opa called successfully")
except AttributeError as e:
self.fail(f"set_style_opa raised AttributeError: {e}")
@@ -79,15 +57,13 @@ class TestKeyboardAnimation(unittest.TestCase):
"""
print("Testing smooth_show animation...")
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
keyboard.add_flag(lv.obj.FLAG.HIDDEN)
self.create_keyboard_scene()
self.keyboard.add_flag(lv.obj.FLAG.HIDDEN)
# This should work without raising AttributeError
try:
mpos.ui.anim.smooth_show(keyboard)
wait_for_render(100)
mpos.ui.anim.smooth_show(self.keyboard)
self.wait_for_render(100)
print("smooth_show called successfully")
except AttributeError as e:
self.fail(f"smooth_show raised AttributeError: {e}\n"
@@ -95,7 +71,7 @@ class TestKeyboardAnimation(unittest.TestCase):
# Verify keyboard is no longer hidden
self.assertFalse(
keyboard.has_flag(lv.obj.FLAG.HIDDEN),
self.keyboard.has_flag(lv.obj.FLAG.HIDDEN),
"Keyboard should not be hidden after smooth_show"
)
@@ -109,15 +85,13 @@ class TestKeyboardAnimation(unittest.TestCase):
"""
print("Testing smooth_hide animation...")
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
self.create_keyboard_scene()
# Start visible
keyboard.remove_flag(lv.obj.FLAG.HIDDEN)
self.keyboard.remove_flag(lv.obj.FLAG.HIDDEN)
# This should work without raising AttributeError
try:
mpos.ui.anim.smooth_hide(keyboard)
mpos.ui.anim.smooth_hide(self.keyboard)
print("smooth_hide called successfully")
except AttributeError as e:
self.fail(f"smooth_hide raised AttributeError: {e}\n"
@@ -135,28 +109,26 @@ class TestKeyboardAnimation(unittest.TestCase):
"""
print("Testing full show/hide cycle...")
keyboard = MposKeyboard(self.screen)
keyboard.set_textarea(self.textarea)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
keyboard.add_flag(lv.obj.FLAG.HIDDEN)
self.create_keyboard_scene()
self.keyboard.add_flag(lv.obj.FLAG.HIDDEN)
# Initial state: hidden
self.assertTrue(keyboard.has_flag(lv.obj.FLAG.HIDDEN))
self.assertTrue(self.keyboard.has_flag(lv.obj.FLAG.HIDDEN))
# Show keyboard (simulates textarea click)
try:
mpos.ui.anim.smooth_show(keyboard)
wait_for_render(100)
mpos.ui.anim.smooth_show(self.keyboard)
self.wait_for_render(100)
except AttributeError as e:
self.fail(f"Failed during smooth_show: {e}")
# Should be visible now
self.assertFalse(keyboard.has_flag(lv.obj.FLAG.HIDDEN))
self.assertFalse(self.keyboard.has_flag(lv.obj.FLAG.HIDDEN))
# Hide keyboard (simulates pressing Enter)
try:
mpos.ui.anim.smooth_hide(keyboard)
wait_for_render(100)
mpos.ui.anim.smooth_hide(self.keyboard)
self.wait_for_render(100)
except AttributeError as e:
self.fail(f"Failed during smooth_hide: {e}")
@@ -170,22 +142,19 @@ class TestKeyboardAnimation(unittest.TestCase):
"""
print("Testing get_y and set_y methods...")
keyboard = MposKeyboard(self.screen)
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
self.create_keyboard_scene()
# Verify methods exist
self.assertTrue(hasattr(keyboard, 'get_y'), "Missing get_y method")
self.assertTrue(hasattr(keyboard, 'set_y'), "Missing set_y method")
self.assertTrue(hasattr(self.keyboard, 'get_y'), "Missing get_y method")
self.assertTrue(hasattr(self.keyboard, 'set_y'), "Missing set_y method")
# Try using them
try:
y = keyboard.get_y()
keyboard.set_y(y + 10)
new_y = keyboard.get_y()
y = self.keyboard.get_y()
self.keyboard.set_y(y + 10)
new_y = self.keyboard.get_y()
print(f"Position test: {y} -> {new_y}")
except AttributeError as e:
self.fail(f"Position methods raised AttributeError: {e}")
print("=== Position methods test PASSED ===")