You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
30b3764710
All frameworks now follow the same singleton class pattern with class methods:
AudioFlinger (already had this pattern)
DownloadManager (refactored)
ConnectivityManager (refactored)
CameraManager (refactored)
SensorManager (refactored)
Pattern Structure:
class FrameworkName:
_initialized = False
_instance_data = {}
@classmethod
def init(cls, *args, **kwargs):
"""Initialize the framework"""
cls._initialized = True
# initialization logic
@classmethod
def is_available(cls):
"""Check if framework is available"""
return cls._initialized
@classmethod
def method_name(cls, *args):
"""Framework methods as class methods"""
# implementation
2. Standardized Imports in __init__.py
All frameworks are now imported consistently as classes:
from .content.package_manager import PackageManager
from .config import SharedPreferences
from .net.connectivity_manager import ConnectivityManager
from .net.wifi_service import WifiService
from .audio.audioflinger import AudioFlinger
from .net.download_manager import DownloadManager
from .task_manager import TaskManager
from .camera_manager import CameraManager
from .sensor_manager import SensorManager
3. Updated Board Initialization Files
Fixed imports in all board files to use the new class-based pattern:
linux.py
fri3d_2024.py
fri3d_2026.py
waveshare_esp32_s3_touch_lcd_2.py
4. Updated UI Components
Fixed topmenu.py to import SensorManager as a class instead of a module.
5. Benefits of This Harmonization
✅ Consistency: All frameworks follow the same pattern - no more mixing of module imports and class imports ✅ Simplicity: Single, clear way to use frameworks - always as classes with class methods ✅ Functionality: All frameworks work identically - init(), is_available(), and other methods are consistent ✅ Maintainability: New developers see one pattern to follow across all frameworks ✅ No Breaking Changes: Apps continue to work without modification (Quasi apps, Lightning Piggy, etc.)
6. Testing
All tests pass successfully, confirming:
Framework initialization works correctly
Board hardware detection functions properly
UI components render without errors
No regressions in existing functionality
The harmonization is complete and production-ready. All frameworks now provide a unified, predictable interface that's easy to understand and extend.
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
# 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:
-
mpos.testing- Hardware and system mocks- Location:
internal_filesystem/lib/mpos/testing/ - Use for: Mocking hardware (Pin, PWM, I2S, NeoPixel), network, async operations
- Location:
-
mpos.ui.testing- LVGL/UI testing utilities- Location:
internal_filesystem/lib/mpos/ui/testing.py - Use for: UI interaction, screenshots, widget inspection
- Location:
Base Test Classes
GraphicalTestBase
Base class for all graphical (LVGL) tests. Provides:
- Automatic screen creation/cleanup
- Screenshot capture
- Widget finding utilities
- Custom assertions
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 taskscapture_screenshot(filename)- Save screenshotfind_label_with_text(text)- Find label widgetclick_button(button)- Simulate button clickassertTextPresent(text)- Assert text is on screenassertWidgetVisible(widget)- Assert widget is visible
KeyboardTestBase
Extends GraphicalTestBase for keyboard tests. Provides:
- Keyboard and textarea creation
- Reliable keyboard button clicking
- Textarea assertions
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 + MposKeyboardclick_keyboard_button(text)- Click keyboard button reliablytype_text(text)- Type a stringget_textarea_text()- Get textarea contentclear_textarea()- Clear textareaassertTextareaText(expected)- Assert textarea contentassertTextareaEmpty()- Assert textarea is empty
Mock Classes
Import mocks from mpos.testing:
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
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:
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:
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:
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 teststest_graphical_*.py- Tests requiring LVGL/UI (detected by unittest.sh)manual_test_*.py- Manual tests (not run automatically)
Writing New Tests
Simple Unit Test
import unittest
class TestMyFeature(unittest.TestCase):
def test_something(self):
result = my_function()
self.assertEqual(result, expected)
Graphical Test
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
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
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
- Use base classes - Extend
GraphicalTestBaseorKeyboardTestBasefor UI tests - Use mpos.testing mocks - Don't create inline mocks; use the centralized ones
- Clean up in tearDown - Base classes handle this, but custom tests should clean up
- Don't include
if __name__ == '__main__'- The test runner handles this - Use descriptive test names -
test_keyboard_q_button_worksnottest_1 - Add docstrings - Explain what the test verifies and why
Debugging Tests
# 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:
cd tests/screenshots
./convert_to_png.sh