diff --git a/internal_filesystem/apps/com.micropythonos.errortest/META-INF/MANIFEST.JSON b/internal_filesystem/apps/com.micropythonos.errortest/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..02aef763 --- /dev/null +++ b/internal_filesystem/apps/com.micropythonos.errortest/META-INF/MANIFEST.JSON @@ -0,0 +1,24 @@ +{ +"name": "ErrorTest", +"publisher": "MicroPythonOS", +"short_description": "Test app with intentional error", +"long_description": "This app has an intentional import error for testing.", +"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.errortest/icons/com.micropythonos.errortest_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.errortest/mpks/com.micropythonos.errortest_0.0.1.mpk", +"fullname": "com.micropythonos.errortest", +"version": "0.0.1", +"category": "development", +"activities": [ + { + "entrypoint": "assets/error.py", + "classname": "Error", + "intent_filters": [ + { + "action": "main", + "category": "launcher" + } + ] + } + ] +} + diff --git a/internal_filesystem/apps/com.micropythonos.errortest/assets/error.py b/internal_filesystem/apps/com.micropythonos.errortest/assets/error.py new file mode 100644 index 00000000..db63482d --- /dev/null +++ b/internal_filesystem/apps/com.micropythonos.errortest/assets/error.py @@ -0,0 +1,10 @@ +from mpos.apps import ActivityDoesntExist # should fail here + +class Error(Activity): + + def onCreate(self): + screen = lv.obj() + label = lv.label(screen) + label.set_text('Hello World!') + label.center() + self.setContentView(screen) diff --git a/scripts/bundle_apps.sh b/scripts/bundle_apps.sh index f02ac318..d22724dd 100755 --- a/scripts/bundle_apps.sh +++ b/scripts/bundle_apps.sh @@ -20,7 +20,8 @@ rm "$outputjson" # com.micropythonos.confetti crashes when closing # com.micropythonos.showfonts is slow to open # com.micropythonos.draw isnt very useful -blacklist="com.micropythonos.filemanager com.quasikili.quasidoodle com.micropythonos.confetti com.micropythonos.showfonts com.micropythonos.draw" +# com.micropythonos.errortest is an intentional bad app for testing (caught by tests/test_graphical_launch_all_apps.py) +blacklist="com.micropythonos.filemanager com.quasikili.quasidoodle com.micropythonos.confetti com.micropythonos.showfonts com.micropythonos.draw com.micropythonos.errortest" echo "[" | tee -a "$outputjson" diff --git a/tests/test_graphical_launch_all_apps.py b/tests/test_graphical_launch_all_apps.py new file mode 100644 index 00000000..8ff924ba --- /dev/null +++ b/tests/test_graphical_launch_all_apps.py @@ -0,0 +1,232 @@ +""" +Test that launches all installed apps to check for startup errors. + +This test discovers all apps in apps/ and builtin/apps/ directories, +launches each one, and checks for exceptions during startup. +""" + +import unittest +import os +import sys +import time + +# This is a graphical test - needs boot and main to run first +# Add tests directory to path for helpers +if '../tests' not in sys.path: + sys.path.insert(0, '../tests') + +from graphical_test_helper import wait_for_render +import mpos.apps +import mpos.ui +from mpos.content.package_manager import PackageManager + + +class TestLaunchAllApps(unittest.TestCase): + """Test launching all installed apps.""" + + def setUp(self): + """Set up test fixtures.""" + self.apps_to_test = [] + self.app_errors = {} + + # Discover all apps + self._discover_apps() + + def _discover_apps(self): + """Discover all installed apps.""" + # Use PackageManager to get all apps + all_packages = PackageManager.get_app_list() + + for package in all_packages: + # Get the main activity for each app + if package.activities: + # Use first activity as the main one (activities are dicts) + main_activity = package.activities[0] + self.apps_to_test.append({ + 'package_name': package.fullname, + 'activity_name': main_activity.get('classname', 'MainActivity'), + 'label': package.name + }) + + def test_launch_all_apps(self): + """Launch each app and check for errors.""" + print(f"\n{'='*60}") + print(f"Testing {len(self.apps_to_test)} apps for startup errors") + print(f"{'='*60}\n") + + failed_apps = [] + passed_apps = [] + + for i, app_info in enumerate(self.apps_to_test, 1): + package_name = app_info['package_name'] + activity_name = app_info['activity_name'] + label = app_info['label'] + + print(f"\n[{i}/{len(self.apps_to_test)}] Testing: {label} ({package_name})") + + error_found = False + error_message = "" + + try: + # Launch the app by package name + result = mpos.apps.start_app(package_name) + + # Wait for UI to render + wait_for_render(iterations=5) + + # Check if start_app returned False (indicates error during execution) + if result is False: + error_found = True + error_message = "App failed to start (execute_script returned False)" + print(f" ❌ FAILED - App failed to start") + print(f" {error_message}") + failed_apps.append({ + 'info': app_info, + 'error': error_message + }) + else: + # If we got here without error, the app loaded successfully + print(f" ✓ PASSED - App loaded successfully") + passed_apps.append(app_info) + + # Navigate back to exit the app + mpos.ui.back_screen() + wait_for_render(iterations=3) + + except Exception as e: + error_found = True + error_message = f"{type(e).__name__}: {str(e)}" + print(f" ❌ FAILED - Exception during launch") + print(f" {error_message}") + failed_apps.append({ + 'info': app_info, + 'error': error_message + }) + + # Print summary + print(f"\n{'='*60}") + print(f"Test Summary") + print(f"{'='*60}") + print(f"Total apps tested: {len(self.apps_to_test)}") + print(f"Passed: {len(passed_apps)}") + print(f"Failed: {len(failed_apps)}") + print(f"{'='*60}\n") + + if failed_apps: + print("Failed apps:") + for fail in failed_apps: + print(f" - {fail['info']['label']} ({fail['info']['package_name']})") + print(f" Error: {fail['error']}") + print() + + # Test should detect at least one error (the intentional errortest app) + if len(failed_apps) > 0: + print(f"✓ Test successfully detected {len(failed_apps)} app(s) with errors") + + # Check if we found the errortest app + errortest_found = any( + 'errortest' in fail['info']['package_name'].lower() + for fail in failed_apps + ) + + if errortest_found: + print("✓ Successfully detected the intentional error in errortest app") + + # Verify errortest was found + all_app_names = [app['package_name'] for app in self.apps_to_test] + has_errortest = any('errortest' in name.lower() for name in all_app_names) + + if has_errortest: + self.assertTrue(errortest_found, + "Failed to detect error in com.micropythonos.errortest app") + else: + print("⚠ Warning: No errors detected. All apps launched successfully.") + + +class TestLaunchSpecificApps(unittest.TestCase): + """Test specific apps individually for more detailed error reporting.""" + + def _launch_and_check_app(self, package_name, expected_error=False): + """ + Launch an app and check for errors. + + Args: + package_name: Full package name (e.g., 'com.micropythonos.camera') + expected_error: Whether this app is expected to have errors + + Returns: + tuple: (success, error_message) + """ + error_found = False + error_message = "" + + try: + # Launch the app by package name + result = mpos.apps.start_app(package_name) + wait_for_render(iterations=5) + + # Check if start_app returned False (indicates error) + if result is False: + error_found = True + error_message = "App failed to start (execute_script returned False)" + + # Navigate back + mpos.ui.back_screen() + wait_for_render(iterations=3) + + except Exception as e: + error_found = True + error_message = f"{type(e).__name__}: {str(e)}" + + if expected_error: + # For apps expected to have errors + return (error_found, error_message) + else: + # For apps that should work + return (not error_found, error_message) + + def test_errortest_app_has_error(self): + """Test that the errortest app properly reports an error.""" + success, error_msg = self._launch_and_check_app( + 'com.micropythonos.errortest', + expected_error=True + ) + + if success: + print(f"\n✓ Successfully detected error in errortest app:") + print(f" {error_msg}") + else: + print(f"\n❌ Failed to detect error in errortest app") + + self.assertTrue(success, + "The errortest app should have an error but none was detected") + + def test_launcher_app_loads(self): + """Test that the launcher app loads without errors.""" + success, error_msg = self._launch_and_check_app( + 'com.micropythonos.launcher', + expected_error=False + ) + + if not success: + print(f"\n❌ Launcher app has errors: {error_msg}") + + self.assertTrue(success, + f"Launcher app should load without errors: {error_msg}") + + def test_about_app_loads(self): + """Test that the About app loads without errors.""" + success, error_msg = self._launch_and_check_app( + 'com.micropythonos.about', + expected_error=False + ) + + if not success: + print(f"\n❌ About app has errors: {error_msg}") + + self.assertTrue(success, + f"About app should load without errors: {error_msg}") + + +if __name__ == '__main__': + unittest.main()