From e94c8ab08483d8996fa49157700cb6b9a623553c Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 7 Dec 2025 09:13:56 +0100 Subject: [PATCH] More tests --- tests/test_calibration_check_bug.py | 162 +++++++++++++++++++ tests/test_imu_calibration_ui_bug.py | 230 +++++++++++++++++++++++++++ 2 files changed, 392 insertions(+) create mode 100644 tests/test_calibration_check_bug.py create mode 100755 tests/test_imu_calibration_ui_bug.py diff --git a/tests/test_calibration_check_bug.py b/tests/test_calibration_check_bug.py new file mode 100644 index 00000000..14e72d80 --- /dev/null +++ b/tests/test_calibration_check_bug.py @@ -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() diff --git a/tests/test_imu_calibration_ui_bug.py b/tests/test_imu_calibration_ui_bug.py new file mode 100755 index 00000000..59e55d70 --- /dev/null +++ b/tests/test_imu_calibration_ui_bug.py @@ -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)