You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
More tests
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
"""Test for calibration check bug after calibrating.
|
||||
|
||||
Reproduces issue where check_calibration_quality() returns None after calibration.
|
||||
"""
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
# Mock hardware before importing SensorManager
|
||||
class MockI2C:
|
||||
def __init__(self, bus_id, sda=None, scl=None):
|
||||
self.bus_id = bus_id
|
||||
self.sda = sda
|
||||
self.scl = scl
|
||||
self.memory = {}
|
||||
|
||||
def readfrom_mem(self, addr, reg, nbytes):
|
||||
if addr not in self.memory:
|
||||
raise OSError("I2C device not found")
|
||||
if reg not in self.memory[addr]:
|
||||
return bytes([0] * nbytes)
|
||||
return bytes(self.memory[addr][reg])
|
||||
|
||||
def writeto_mem(self, addr, reg, data):
|
||||
if addr not in self.memory:
|
||||
self.memory[addr] = {}
|
||||
self.memory[addr][reg] = list(data)
|
||||
|
||||
|
||||
class MockQMI8658:
|
||||
def __init__(self, i2c_bus, address=0x6B, accel_scale=0b10, gyro_scale=0b100):
|
||||
self.i2c = i2c_bus
|
||||
self.address = address
|
||||
self.accel_scale = accel_scale
|
||||
self.gyro_scale = gyro_scale
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
return 25.5
|
||||
|
||||
@property
|
||||
def acceleration(self):
|
||||
return (0.0, 0.0, 1.0) # At rest, Z-axis = 1G
|
||||
|
||||
@property
|
||||
def gyro(self):
|
||||
return (0.0, 0.0, 0.0) # Stationary
|
||||
|
||||
|
||||
# Mock constants
|
||||
_QMI8685_PARTID = 0x05
|
||||
_REG_PARTID = 0x00
|
||||
_ACCELSCALE_RANGE_8G = 0b10
|
||||
_GYROSCALE_RANGE_256DPS = 0b100
|
||||
|
||||
# Create mock modules
|
||||
mock_machine = type('module', (), {
|
||||
'I2C': MockI2C,
|
||||
'Pin': type('Pin', (), {})
|
||||
})()
|
||||
|
||||
mock_qmi8658 = type('module', (), {
|
||||
'QMI8658': MockQMI8658,
|
||||
'_QMI8685_PARTID': _QMI8685_PARTID,
|
||||
'_REG_PARTID': _REG_PARTID,
|
||||
'_ACCELSCALE_RANGE_8G': _ACCELSCALE_RANGE_8G,
|
||||
'_GYROSCALE_RANGE_256DPS': _GYROSCALE_RANGE_256DPS
|
||||
})()
|
||||
|
||||
def _mock_mcu_temperature(*args, **kwargs):
|
||||
return 42.0
|
||||
|
||||
mock_esp32 = type('module', (), {
|
||||
'mcu_temperature': _mock_mcu_temperature
|
||||
})()
|
||||
|
||||
# Inject mocks
|
||||
sys.modules['machine'] = mock_machine
|
||||
sys.modules['mpos.hardware.drivers.qmi8658'] = mock_qmi8658
|
||||
sys.modules['esp32'] = mock_esp32
|
||||
|
||||
try:
|
||||
import _thread
|
||||
except ImportError:
|
||||
mock_thread = type('module', (), {
|
||||
'allocate_lock': lambda: type('lock', (), {
|
||||
'acquire': lambda self: None,
|
||||
'release': lambda self: None
|
||||
})()
|
||||
})()
|
||||
sys.modules['_thread'] = mock_thread
|
||||
|
||||
# Now import the module to test
|
||||
import mpos.sensor_manager as SensorManager
|
||||
|
||||
|
||||
class TestCalibrationCheckBug(unittest.TestCase):
|
||||
"""Test case for calibration check bug."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test."""
|
||||
# Reset SensorManager state
|
||||
SensorManager._initialized = False
|
||||
SensorManager._imu_driver = None
|
||||
SensorManager._sensor_list = []
|
||||
SensorManager._has_mcu_temperature = False
|
||||
|
||||
# Create mock I2C bus with QMI8658
|
||||
self.i2c_bus = MockI2C(0, sda=48, scl=47)
|
||||
self.i2c_bus.memory[0x6B] = {_REG_PARTID: [_QMI8685_PARTID]}
|
||||
|
||||
def test_check_quality_after_calibration(self):
|
||||
"""Test that check_calibration_quality() works after calibration.
|
||||
|
||||
This reproduces the bug where check_calibration_quality() returns
|
||||
None or shows "--" after performing calibration.
|
||||
"""
|
||||
# Initialize
|
||||
SensorManager.init(self.i2c_bus, address=0x6B)
|
||||
|
||||
# Step 1: Check calibration quality BEFORE calibration (should work)
|
||||
print("\n=== Step 1: Check quality BEFORE calibration ===")
|
||||
quality_before = SensorManager.check_calibration_quality(samples=10)
|
||||
self.assertIsNotNone(quality_before, "Quality check BEFORE calibration should return data")
|
||||
self.assertIn('quality_score', quality_before)
|
||||
print(f"Quality before: {quality_before['quality_rating']} ({quality_before['quality_score']:.2f})")
|
||||
|
||||
# Step 2: Calibrate sensors
|
||||
print("\n=== Step 2: Calibrate sensors ===")
|
||||
accel = SensorManager.get_default_sensor(SensorManager.TYPE_ACCELEROMETER)
|
||||
gyro = SensorManager.get_default_sensor(SensorManager.TYPE_GYROSCOPE)
|
||||
|
||||
self.assertIsNotNone(accel, "Accelerometer should be available")
|
||||
self.assertIsNotNone(gyro, "Gyroscope should be available")
|
||||
|
||||
accel_offsets = SensorManager.calibrate_sensor(accel, samples=10)
|
||||
print(f"Accel offsets: {accel_offsets}")
|
||||
self.assertIsNotNone(accel_offsets, "Accelerometer calibration should succeed")
|
||||
|
||||
gyro_offsets = SensorManager.calibrate_sensor(gyro, samples=10)
|
||||
print(f"Gyro offsets: {gyro_offsets}")
|
||||
self.assertIsNotNone(gyro_offsets, "Gyroscope calibration should succeed")
|
||||
|
||||
# Step 3: Check calibration quality AFTER calibration (BUG: returns None)
|
||||
print("\n=== Step 3: Check quality AFTER calibration ===")
|
||||
quality_after = SensorManager.check_calibration_quality(samples=10)
|
||||
self.assertIsNotNone(quality_after, "Quality check AFTER calibration should return data (BUG: returns None)")
|
||||
self.assertIn('quality_score', quality_after)
|
||||
print(f"Quality after: {quality_after['quality_rating']} ({quality_after['quality_score']:.2f})")
|
||||
|
||||
# Verify sensor reads still work
|
||||
print("\n=== Step 4: Verify sensor reads still work ===")
|
||||
accel_data = SensorManager.read_sensor(accel)
|
||||
self.assertIsNotNone(accel_data, "Accelerometer should still be readable")
|
||||
print(f"Accel data: {accel_data}")
|
||||
|
||||
gyro_data = SensorManager.read_sensor(gyro)
|
||||
self.assertIsNotNone(gyro_data, "Gyroscope should still be readable")
|
||||
print(f"Gyro data: {gyro_data}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Executable
+230
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Automated UI test for IMU calibration bug.
|
||||
|
||||
Tests the complete flow:
|
||||
1. Open Settings → IMU → Check Calibration
|
||||
2. Verify values are shown
|
||||
3. Click "Calibrate" → Calibrate IMU
|
||||
4. Click "Calibrate Now"
|
||||
5. Go back to Check Calibration
|
||||
6. BUG: Verify values are shown (not "--")
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Import graphical test infrastructure
|
||||
import lvgl as lv
|
||||
from mpos.ui.testing import (
|
||||
wait_for_render,
|
||||
simulate_click,
|
||||
find_button_with_text,
|
||||
find_label_with_text,
|
||||
get_widget_coords,
|
||||
print_screen_labels,
|
||||
capture_screenshot
|
||||
)
|
||||
|
||||
def click_button(button_text, timeout=5):
|
||||
"""Find and click a button with given text."""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
button = find_button_with_text(lv.screen_active(), button_text)
|
||||
if button:
|
||||
coords = get_widget_coords(button)
|
||||
if coords:
|
||||
print(f"Clicking button '{button_text}' at ({coords['center_x']}, {coords['center_y']})")
|
||||
simulate_click(coords['center_x'], coords['center_y'])
|
||||
wait_for_render(iterations=20)
|
||||
return True
|
||||
wait_for_render(iterations=5)
|
||||
print(f"ERROR: Button '{button_text}' not found after {timeout}s")
|
||||
return False
|
||||
|
||||
def click_label(label_text, timeout=5):
|
||||
"""Find a label with given text and click on it (or its clickable parent)."""
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
label = find_label_with_text(lv.screen_active(), label_text)
|
||||
if label:
|
||||
coords = get_widget_coords(label)
|
||||
if coords:
|
||||
print(f"Clicking label '{label_text}' at ({coords['center_x']}, {coords['center_y']})")
|
||||
simulate_click(coords['center_x'], coords['center_y'])
|
||||
wait_for_render(iterations=20)
|
||||
return True
|
||||
wait_for_render(iterations=5)
|
||||
print(f"ERROR: Label '{label_text}' not found after {timeout}s")
|
||||
return False
|
||||
|
||||
def find_text_on_screen(text):
|
||||
"""Check if text is present on screen."""
|
||||
return find_label_with_text(lv.screen_active(), text) is not None
|
||||
|
||||
def main():
|
||||
print("=== IMU Calibration UI Bug Test ===\n")
|
||||
|
||||
# Initialize the OS (boot.py and main.py)
|
||||
print("Step 1: Initializing MicroPythonOS...")
|
||||
import mpos.main
|
||||
wait_for_render(iterations=30)
|
||||
print("OS initialized\n")
|
||||
|
||||
# Step 2: Open Settings app
|
||||
print("Step 2: Opening Settings app...")
|
||||
import mpos.apps
|
||||
|
||||
# Start Settings app by name
|
||||
mpos.apps.start_app("com.micropythonos.settings")
|
||||
wait_for_render(iterations=30)
|
||||
print("Settings app opened\n")
|
||||
|
||||
print("Current screen content:")
|
||||
print_screen_labels(lv.screen_active())
|
||||
print()
|
||||
|
||||
# Check if we're on the main Settings screen (should see multiple settings options)
|
||||
# The Settings app shows a list with items like "Calibrate IMU", "Check IMU Calibration", "Theme Color", etc.
|
||||
on_settings_main = (find_text_on_screen("Calibrate IMU") and
|
||||
find_text_on_screen("Check IMU Calibration") and
|
||||
find_text_on_screen("Theme Color"))
|
||||
|
||||
# If we're on a sub-screen (like Calibrate IMU or Check IMU Calibration screens),
|
||||
# we need to go back to Settings main. We can detect this by looking for screen titles.
|
||||
if not on_settings_main:
|
||||
print("Step 3: Not on Settings main screen, clicking Back to return...")
|
||||
if not click_button("Back"):
|
||||
print("WARNING: Could not find Back button, trying Cancel...")
|
||||
if not click_button("Cancel"):
|
||||
print("FAILED: Could not navigate back to Settings main")
|
||||
return False
|
||||
wait_for_render(iterations=20)
|
||||
print("Current screen content:")
|
||||
print_screen_labels(lv.screen_active())
|
||||
print()
|
||||
|
||||
# Step 4: Click "Check IMU Calibration" (it's a clickable label/container, not a button)
|
||||
print("Step 4: Clicking 'Check IMU Calibration' menu item...")
|
||||
if not click_label("Check IMU Calibration"):
|
||||
print("FAILED: Could not find Check IMU Calibration menu item")
|
||||
return False
|
||||
print("Check IMU Calibration opened\n")
|
||||
|
||||
# Wait for quality check to complete
|
||||
time.sleep(0.5)
|
||||
wait_for_render(iterations=30)
|
||||
|
||||
print("Step 5: Checking BEFORE calibration...")
|
||||
print("Current screen content:")
|
||||
print_screen_labels(lv.screen_active())
|
||||
print()
|
||||
|
||||
# Capture screenshot before
|
||||
capture_screenshot("../tests/screenshots/check_imu_before_calib.raw")
|
||||
|
||||
# Look for actual values (not "--")
|
||||
has_values_before = False
|
||||
widgets = []
|
||||
from mpos.ui.testing import get_all_widgets_with_text
|
||||
for widget in get_all_widgets_with_text(lv.screen_active()):
|
||||
text = widget.get_text()
|
||||
# Look for patterns like "X: 0.00" or "Quality: Good"
|
||||
if ":" in text and "--" not in text:
|
||||
if any(char.isdigit() for char in text):
|
||||
print(f"Found value: {text}")
|
||||
has_values_before = True
|
||||
|
||||
if not has_values_before:
|
||||
print("WARNING: No values found before calibration (all showing '--')")
|
||||
else:
|
||||
print("GOOD: Values are showing before calibration")
|
||||
print()
|
||||
|
||||
# Step 6: Click "Calibrate" button to go to calibration screen
|
||||
print("Step 6: Finding 'Calibrate' button...")
|
||||
calibrate_btn = find_button_with_text(lv.screen_active(), "Calibrate")
|
||||
if not calibrate_btn:
|
||||
print("FAILED: Could not find Calibrate button")
|
||||
return False
|
||||
|
||||
print(f"Found Calibrate button: {calibrate_btn}")
|
||||
print("Manually sending CLICKED event to button...")
|
||||
# Instead of using simulate_click, manually send the event
|
||||
calibrate_btn.send_event(lv.EVENT.CLICKED, None)
|
||||
wait_for_render(iterations=20)
|
||||
|
||||
# Wait for navigation to complete (activity transition can take some time)
|
||||
time.sleep(0.5)
|
||||
wait_for_render(iterations=50)
|
||||
print("Calibrate IMU screen should be open now\n")
|
||||
|
||||
print("Current screen content:")
|
||||
print_screen_labels(lv.screen_active())
|
||||
print()
|
||||
|
||||
# Step 7: Click "Calibrate Now" button
|
||||
print("Step 7: Clicking 'Calibrate Now' button...")
|
||||
if not click_button("Calibrate Now"):
|
||||
print("FAILED: Could not find 'Calibrate Now' button")
|
||||
return False
|
||||
print("Calibration started...\n")
|
||||
|
||||
# Wait for calibration to complete (~2 seconds + UI updates)
|
||||
time.sleep(3)
|
||||
wait_for_render(iterations=50)
|
||||
|
||||
print("Current screen content after calibration:")
|
||||
print_screen_labels(lv.screen_active())
|
||||
print()
|
||||
|
||||
# Step 8: Click "Done" to go back
|
||||
print("Step 8: Clicking 'Done' button...")
|
||||
if not click_button("Done"):
|
||||
print("FAILED: Could not find Done button")
|
||||
return False
|
||||
print("Going back to Check Calibration\n")
|
||||
|
||||
# Wait for screen to load
|
||||
time.sleep(0.5)
|
||||
wait_for_render(iterations=30)
|
||||
|
||||
# Step 9: Check AFTER calibration (BUG: should show values, not "--")
|
||||
print("Step 9: Checking AFTER calibration (testing for bug)...")
|
||||
print("Current screen content:")
|
||||
print_screen_labels(lv.screen_active())
|
||||
print()
|
||||
|
||||
# Capture screenshot after
|
||||
capture_screenshot("../tests/screenshots/check_imu_after_calib.raw")
|
||||
|
||||
# Look for actual values (not "--")
|
||||
has_values_after = False
|
||||
for widget in get_all_widgets_with_text(lv.screen_active()):
|
||||
text = widget.get_text()
|
||||
# Look for patterns like "X: 0.00" or "Quality: Good"
|
||||
if ":" in text and "--" not in text:
|
||||
if any(char.isdigit() for char in text):
|
||||
print(f"Found value: {text}")
|
||||
has_values_after = True
|
||||
|
||||
print()
|
||||
print("="*60)
|
||||
print("TEST RESULTS:")
|
||||
print(f" Values shown BEFORE calibration: {has_values_before}")
|
||||
print(f" Values shown AFTER calibration: {has_values_after}")
|
||||
|
||||
if has_values_before and not has_values_after:
|
||||
print("\n ❌ BUG REPRODUCED: Values disappeared after calibration!")
|
||||
print(" Expected: Values should still be shown")
|
||||
print(" Actual: All showing '--'")
|
||||
return False
|
||||
elif has_values_after:
|
||||
print("\n ✅ PASS: Values are showing correctly after calibration")
|
||||
return True
|
||||
else:
|
||||
print("\n ⚠️ WARNING: No values shown before or after (might be desktop mock issue)")
|
||||
return True
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user