Skip to content

Commit 08d1b28

Browse files
Update tests
1 parent a31ac2f commit 08d1b28

File tree

5 files changed

+376
-163
lines changed

5 files changed

+376
-163
lines changed

internal_filesystem/lib/mpos/testing/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
MockI2S,
2525
MockTimer,
2626
MockSocket,
27+
MockNeoPixel,
2728

2829
# MPOS mocks
2930
MockTaskManager,
@@ -58,6 +59,7 @@
5859
'MockI2S',
5960
'MockTimer',
6061
'MockSocket',
62+
'MockNeoPixel',
6163

6264
# MPOS mocks
6365
'MockTaskManager',

internal_filesystem/lib/mpos/testing/mocks.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,50 @@ def reset_all(cls):
204204
cls._all_timers.clear()
205205

206206

207+
class MockNeoPixel:
208+
"""Mock neopixel.NeoPixel for testing LED operations."""
209+
210+
def __init__(self, pin, num_leds, bpp=3, timing=1):
211+
self.pin = pin
212+
self.num_leds = num_leds
213+
self.bpp = bpp
214+
self.timing = timing
215+
self.pixels = [(0, 0, 0)] * num_leds
216+
self.write_count = 0
217+
218+
def __setitem__(self, index, value):
219+
"""Set LED color (R, G, B) or (R, G, B, W) tuple."""
220+
if 0 <= index < self.num_leds:
221+
self.pixels[index] = value
222+
223+
def __getitem__(self, index):
224+
"""Get LED color."""
225+
if 0 <= index < self.num_leds:
226+
return self.pixels[index]
227+
return (0, 0, 0)
228+
229+
def __len__(self):
230+
"""Return number of LEDs."""
231+
return self.num_leds
232+
233+
def fill(self, color):
234+
"""Fill all LEDs with the same color."""
235+
for i in range(self.num_leds):
236+
self.pixels[i] = color
237+
238+
def write(self):
239+
"""Update hardware (mock - just increment counter)."""
240+
self.write_count += 1
241+
242+
def get_all_colors(self):
243+
"""Get all LED colors (for testing assertions)."""
244+
return self.pixels.copy()
245+
246+
def reset_write_count(self):
247+
"""Reset the write counter (for testing)."""
248+
self.write_count = 0
249+
250+
207251
class MockMachine:
208252
"""
209253
Mock machine module containing all hardware mocks.

tests/README.md

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
# MicroPythonOS Testing Guide
2+
3+
This directory contains the test suite for MicroPythonOS. Tests can run on both desktop (for fast iteration) and on-device (for hardware verification).
4+
5+
## Quick Start
6+
7+
```bash
8+
# Run all tests
9+
./tests/unittest.sh
10+
11+
# Run a specific test
12+
./tests/unittest.sh tests/test_graphical_keyboard_q_button_bug.py
13+
14+
# Run on device
15+
./tests/unittest.sh tests/test_graphical_keyboard_q_button_bug.py --ondevice
16+
```
17+
18+
## Test Architecture
19+
20+
### Directory Structure
21+
22+
```
23+
tests/
24+
├── base/ # Base test classes (DRY patterns)
25+
│ ├── __init__.py # Exports GraphicalTestBase, KeyboardTestBase
26+
│ ├── graphical_test_base.py
27+
│ └── keyboard_test_base.py
28+
├── screenshots/ # Captured screenshots for visual regression
29+
├── test_*.py # Test files
30+
├── unittest.sh # Test runner script
31+
└── README.md # This file
32+
```
33+
34+
### Testing Modules
35+
36+
MicroPythonOS provides two testing modules:
37+
38+
1. **`mpos.testing`** - Hardware and system mocks
39+
- Location: `internal_filesystem/lib/mpos/testing/`
40+
- Use for: Mocking hardware (Pin, PWM, I2S, NeoPixel), network, async operations
41+
42+
2. **`mpos.ui.testing`** - LVGL/UI testing utilities
43+
- Location: `internal_filesystem/lib/mpos/ui/testing.py`
44+
- Use for: UI interaction, screenshots, widget inspection
45+
46+
## Base Test Classes
47+
48+
### GraphicalTestBase
49+
50+
Base class for all graphical (LVGL) tests. Provides:
51+
- Automatic screen creation/cleanup
52+
- Screenshot capture
53+
- Widget finding utilities
54+
- Custom assertions
55+
56+
```python
57+
from base import GraphicalTestBase
58+
59+
class TestMyUI(GraphicalTestBase):
60+
def test_something(self):
61+
# self.screen is already created
62+
label = lv.label(self.screen)
63+
label.set_text("Hello")
64+
65+
self.wait_for_render()
66+
self.assertTextPresent("Hello")
67+
self.capture_screenshot("my_test.raw")
68+
```
69+
70+
**Key Methods:**
71+
- `wait_for_render(iterations=5)` - Process LVGL tasks
72+
- `capture_screenshot(filename)` - Save screenshot
73+
- `find_label_with_text(text)` - Find label widget
74+
- `click_button(button)` - Simulate button click
75+
- `assertTextPresent(text)` - Assert text is on screen
76+
- `assertWidgetVisible(widget)` - Assert widget is visible
77+
78+
### KeyboardTestBase
79+
80+
Extends GraphicalTestBase for keyboard tests. Provides:
81+
- Keyboard and textarea creation
82+
- Reliable keyboard button clicking
83+
- Textarea assertions
84+
85+
```python
86+
from base import KeyboardTestBase
87+
88+
class TestMyKeyboard(KeyboardTestBase):
89+
def test_typing(self):
90+
self.create_keyboard_scene()
91+
92+
self.click_keyboard_button("h")
93+
self.click_keyboard_button("i")
94+
95+
self.assertTextareaText("hi")
96+
```
97+
98+
**Key Methods:**
99+
- `create_keyboard_scene()` - Create textarea + MposKeyboard
100+
- `click_keyboard_button(text)` - Click keyboard button reliably
101+
- `type_text(text)` - Type a string
102+
- `get_textarea_text()` - Get textarea content
103+
- `clear_textarea()` - Clear textarea
104+
- `assertTextareaText(expected)` - Assert textarea content
105+
- `assertTextareaEmpty()` - Assert textarea is empty
106+
107+
## Mock Classes
108+
109+
Import mocks from `mpos.testing`:
110+
111+
```python
112+
from mpos.testing import (
113+
# Hardware mocks
114+
MockMachine, # Full machine module mock
115+
MockPin, # GPIO pins
116+
MockPWM, # PWM for buzzer
117+
MockI2S, # Audio I2S
118+
MockTimer, # Hardware timers
119+
MockNeoPixel, # LED strips
120+
MockSocket, # Network sockets
121+
122+
# MPOS mocks
123+
MockTaskManager, # Async task management
124+
MockDownloadManager, # HTTP downloads
125+
126+
# Network mocks
127+
MockNetwork, # WiFi/network module
128+
MockRequests, # HTTP requests
129+
MockResponse, # HTTP responses
130+
131+
# Utility mocks
132+
MockTime, # Time functions
133+
MockJSON, # JSON parsing
134+
135+
# Helpers
136+
inject_mocks, # Inject mocks into sys.modules
137+
create_mock_module, # Create mock module
138+
)
139+
```
140+
141+
### Injecting Mocks
142+
143+
```python
144+
from mpos.testing import inject_mocks, MockMachine, MockNetwork
145+
146+
# Inject before importing modules that use hardware
147+
inject_mocks({
148+
'machine': MockMachine(),
149+
'network': MockNetwork(connected=True),
150+
})
151+
152+
# Now import the module under test
153+
from mpos.hardware import some_module
154+
```
155+
156+
### Mock Examples
157+
158+
**MockNeoPixel:**
159+
```python
160+
from mpos.testing import MockNeoPixel, MockPin
161+
162+
pin = MockPin(5)
163+
leds = MockNeoPixel(pin, 10)
164+
165+
leds[0] = (255, 0, 0) # Set first LED to red
166+
leds.write()
167+
168+
assert leds.write_count == 1
169+
assert leds[0] == (255, 0, 0)
170+
```
171+
172+
**MockRequests:**
173+
```python
174+
from mpos.testing import MockRequests
175+
176+
mock_requests = MockRequests()
177+
mock_requests.set_next_response(
178+
status_code=200,
179+
text='{"status": "ok"}',
180+
headers={'Content-Type': 'application/json'}
181+
)
182+
183+
response = mock_requests.get("https://api.example.com/data")
184+
assert response.status_code == 200
185+
```
186+
187+
**MockTimer:**
188+
```python
189+
from mpos.testing import MockTimer
190+
191+
timer = MockTimer(0)
192+
timer.init(period=1000, mode=MockTimer.PERIODIC, callback=my_callback)
193+
194+
# Manually trigger for testing
195+
timer.trigger()
196+
197+
# Or trigger all timers
198+
MockTimer.trigger_all()
199+
```
200+
201+
## Test Naming Conventions
202+
203+
- `test_*.py` - Standard unit tests
204+
- `test_graphical_*.py` - Tests requiring LVGL/UI (detected by unittest.sh)
205+
- `manual_test_*.py` - Manual tests (not run automatically)
206+
207+
## Writing New Tests
208+
209+
### Simple Unit Test
210+
211+
```python
212+
import unittest
213+
214+
class TestMyFeature(unittest.TestCase):
215+
def test_something(self):
216+
result = my_function()
217+
self.assertEqual(result, expected)
218+
```
219+
220+
### Graphical Test
221+
222+
```python
223+
from base import GraphicalTestBase
224+
import lvgl as lv
225+
226+
class TestMyUI(GraphicalTestBase):
227+
def test_button_click(self):
228+
button = lv.button(self.screen)
229+
label = lv.label(button)
230+
label.set_text("Click Me")
231+
232+
self.wait_for_render()
233+
self.click_button(button)
234+
235+
# Verify result
236+
```
237+
238+
### Keyboard Test
239+
240+
```python
241+
from base import KeyboardTestBase
242+
243+
class TestMyKeyboard(KeyboardTestBase):
244+
def test_input(self):
245+
self.create_keyboard_scene()
246+
247+
self.type_text("hello")
248+
self.assertTextareaText("hello")
249+
250+
self.click_keyboard_button("Enter")
251+
```
252+
253+
### Test with Mocks
254+
255+
```python
256+
import unittest
257+
from mpos.testing import MockNetwork, inject_mocks
258+
259+
class TestNetworkFeature(unittest.TestCase):
260+
def setUp(self):
261+
self.mock_network = MockNetwork(connected=True)
262+
inject_mocks({'network': self.mock_network})
263+
264+
def test_connected(self):
265+
from my_module import check_connection
266+
self.assertTrue(check_connection())
267+
268+
def test_disconnected(self):
269+
self.mock_network.set_connected(False)
270+
from my_module import check_connection
271+
self.assertFalse(check_connection())
272+
```
273+
274+
## Best Practices
275+
276+
1. **Use base classes** - Extend `GraphicalTestBase` or `KeyboardTestBase` for UI tests
277+
2. **Use mpos.testing mocks** - Don't create inline mocks; use the centralized ones
278+
3. **Clean up in tearDown** - Base classes handle this, but custom tests should clean up
279+
4. **Don't include `if __name__ == '__main__'`** - The test runner handles this
280+
5. **Use descriptive test names** - `test_keyboard_q_button_works` not `test_1`
281+
6. **Add docstrings** - Explain what the test verifies and why
282+
283+
## Debugging Tests
284+
285+
```bash
286+
# Run with verbose output
287+
./tests/unittest.sh tests/test_my_test.py
288+
289+
# Run with GDB (desktop only)
290+
gdb --args ./lvgl_micropython/build/lvgl_micropy_unix -X heapsize=8M tests/test_my_test.py
291+
```
292+
293+
## Screenshots
294+
295+
Screenshots are saved to `tests/screenshots/` in raw format. Convert to PNG:
296+
297+
```bash
298+
cd tests/screenshots
299+
./convert_to_png.sh
300+
```

0 commit comments

Comments
 (0)