From b092ff80f1d350a71c8ce3a769abca1f5c2a6c4a Mon Sep 17 00:00:00 2001 From: Dylan McCall Date: Wed, 1 Feb 2023 17:16:36 -0800 Subject: [PATCH 1/4] Move Android-specific code out of main_activity https://phabricator.endlessm.com/T34155 --- src/kolibri_android/android_utils.py | 21 +++++++++++ src/kolibri_android/main_activity/activity.py | 36 ++++--------------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/kolibri_android/android_utils.py b/src/kolibri_android/android_utils.py index 3fab132e..06d54c91 100644 --- a/src/kolibri_android/android_utils.py +++ b/src/kolibri_android/android_utils.py @@ -36,6 +36,7 @@ Environment = autoclass("android.os.Environment") File = autoclass("java.io.File") FileProvider = autoclass("androidx.core.content.FileProvider") +FullScreen = autoclass("org.learningequality.FullScreen") Intent = autoclass("android.content.Intent") NotificationBuilder = autoclass("android.app.Notification$Builder") NotificationManager = autoclass("android.app.NotificationManager") @@ -816,6 +817,26 @@ def apply_android_workarounds(): _android11_ext_storage_workarounds() +@Runnable +def configure_webview(load_fn, load_with_usb_fn, loading_ready_fn): + FullScreen.configureWebview( + PythonActivity.mActivity, + Runnable(load_fn), + Runnable(load_with_usb_fn), + Runnable(loading_ready_fn), + ) + + +@Runnable +def load_url_in_webview(url): + PythonActivity.mWebView.loadUrl(url) + + +@Runnable +def evaluate_javascript(js_code): + PythonActivity.mWebView.evaluateJavascript(js_code, None) + + class StartupState(Enum): FIRST_TIME = auto() USB_USER = auto() diff --git a/src/kolibri_android/main_activity/activity.py b/src/kolibri_android/main_activity/activity.py index db7ecd36..205c602d 100644 --- a/src/kolibri_android/main_activity/activity.py +++ b/src/kolibri_android/main_activity/activity.py @@ -1,11 +1,12 @@ import logging import time -from jnius import autoclass - from ..android_utils import choose_endless_key_uris +from ..android_utils import configure_webview +from ..android_utils import evaluate_javascript from ..android_utils import get_endless_key_uris from ..android_utils import has_any_external_storage_device +from ..android_utils import load_url_in_webview from ..android_utils import PermissionsCancelledError from ..android_utils import PermissionsWrongFolderError from ..android_utils import provision_endless_key_database @@ -15,30 +16,6 @@ from ..android_utils import stat_file from ..application import BaseActivity from ..kolibri_utils import init_kolibri -from ..runnable import Runnable - - -PythonActivity = autoclass("org.kivy.android.PythonActivity") -FullScreen = autoclass("org.learningequality.FullScreen") - - -@Runnable -def configure_webview( - activity, load_runnable, load_with_usb_runnable, loading_ready_runnable -): - FullScreen.configureWebview( - activity, load_runnable, load_with_usb_runnable, loading_ready_runnable - ) - - -@Runnable -def load_url_in_webview(url): - PythonActivity.mWebView.loadUrl(url) - - -@Runnable -def evaluate_javascript(js_code): - PythonActivity.mWebView.evaluateJavascript(js_code, None) def is_endless_key_reachable(): @@ -88,10 +65,9 @@ def __init__(self): super().__init__() configure_webview( - PythonActivity.mActivity, - Runnable(self._on_load), - Runnable(self._on_load_with_usb), - Runnable(self._on_loading_ready), + self._on_load, + self._on_load_with_usb, + self._on_loading_ready, ) def on_activity_stopped(self, activity): From 5c767e863cfb22ae8926389f62558da7e0d70247 Mon Sep 17 00:00:00 2001 From: Dylan McCall Date: Wed, 1 Feb 2023 17:19:28 -0800 Subject: [PATCH 2/4] Add a Python tests directory These tests can be run with `make check`, although they should be run on a system with the same version of Python as the target. https://phabricator.endlessm.com/T34155 --- Makefile | 10 ++++++++-- src/kolibri_android/tests/__init__.py | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/kolibri_android/tests/__init__.py diff --git a/Makefile b/Makefile index c55d2c2d..38c002d0 100644 --- a/Makefile +++ b/Makefile @@ -154,8 +154,7 @@ dist/version.json: needs-version mkdir -p dist echo '{"versionCode": "$(VERSION_CODE)", "versionName": "$(VERSION_NAME)", "ekVersion": "$(EK_VERSION)"}' > $@ -DIST_DEPS = \ - p4a_android_distro \ +BUILD_DEPS = \ src/kolibri \ src/apps-bundle \ src/collections \ @@ -163,6 +162,13 @@ DIST_DEPS = \ needs-version \ dist/version.json +DIST_DEPS = \ + p4a_android_distro \ + $(BUILD_DEPS) + +check: $(BUILD_DEPS) + python3 -m unittest discover kolibri_android.tests -t src + .PHONY: kolibri.apk # Build the signed version of the apk kolibri.apk: $(DIST_DEPS) diff --git a/src/kolibri_android/tests/__init__.py b/src/kolibri_android/tests/__init__.py new file mode 100644 index 00000000..d7e0eee7 --- /dev/null +++ b/src/kolibri_android/tests/__init__.py @@ -0,0 +1,9 @@ +import importlib.util +import sys +from pathlib import Path + +_kolibri_dist_spec = importlib.util.find_spec("kolibri.dist") + +if _kolibri_dist_spec and _kolibri_dist_spec.has_location: + kolibri_dist_path = Path(_kolibri_dist_spec.origin).parent + sys.path.append(kolibri_dist_path.as_posix()) From f64677aabbac20f39feb049e61d25a01dcd3b826 Mon Sep 17 00:00:00 2001 From: Dylan McCall Date: Wed, 1 Feb 2023 17:53:09 -0800 Subject: [PATCH 3/4] Add the ability to run tests directly This will be useful to implement a self-test system that executes a particular set of tests on the target device. https://phabricator.endlessm.com/T34155 --- src/kolibri_android/tests/__main__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/kolibri_android/tests/__main__.py diff --git a/src/kolibri_android/tests/__main__.py b/src/kolibri_android/tests/__main__.py new file mode 100644 index 00000000..ac8bd4bf --- /dev/null +++ b/src/kolibri_android/tests/__main__.py @@ -0,0 +1,18 @@ +import unittest +from pathlib import Path + + +def main(): + this_dir = Path(__file__).parent + + selftest_loader = unittest.TestLoader() + selftests = selftest_loader.discover(this_dir) + + selftest_runner = unittest.runner.TextTestRunner() + selftest_runner.run(selftests) + + selftest_suite = unittest.TestSuite() + + +if __name__ == "__main__": + main() From 56c8f545389ff82330b3ee009c19e5ca6b97c644 Mon Sep 17 00:00:00 2001 From: Dylan McCall Date: Wed, 1 Feb 2023 17:54:45 -0800 Subject: [PATCH 4/4] Add a test that starts MainActivity To run on a build system, this requires mocking Android-specific APIs. https://phabricator.endlessm.com/T34155 --- src/kolibri_android/tests/__main__.py | 2 - .../tests/test_main_activity.py | 68 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/kolibri_android/tests/test_main_activity.py diff --git a/src/kolibri_android/tests/__main__.py b/src/kolibri_android/tests/__main__.py index ac8bd4bf..13fb064a 100644 --- a/src/kolibri_android/tests/__main__.py +++ b/src/kolibri_android/tests/__main__.py @@ -11,8 +11,6 @@ def main(): selftest_runner = unittest.runner.TextTestRunner() selftest_runner.run(selftests) - selftest_suite = unittest.TestSuite() - if __name__ == "__main__": main() diff --git a/src/kolibri_android/tests/test_main_activity.py b/src/kolibri_android/tests/test_main_activity.py new file mode 100644 index 00000000..21442eeb --- /dev/null +++ b/src/kolibri_android/tests/test_main_activity.py @@ -0,0 +1,68 @@ +import os +import sys +import tempfile +from unittest import TestCase +from unittest.mock import DEFAULT +from unittest.mock import MagicMock +from unittest.mock import patch + +# TODO: Only mock these modules when Android is unavailable +android_mock = MagicMock() +jnius_mock = MagicMock() +runnable_mock = MagicMock() + + +@patch.dict( + sys.modules, + { + "android.activity": android_mock, + "jnius": jnius_mock, + "kolibri_android.runnable": runnable_mock, + }, +) +class MainActivityTestCase(TestCase): + def setUp(self): + self.kolibri_home_tempdir = tempfile.TemporaryDirectory() + os.environ["KOLIBRI_HOME"] = self.kolibri_home_tempdir.name + + def tearDown(self): + self.kolibri_home_tempdir.cleanup() + self.kolibri_home_tempdir = None + + @patch.multiple( + "kolibri_android.android_utils", + configure_webview=DEFAULT, + get_signature_key_issuing_organization=DEFAULT, + get_timezone_name=DEFAULT, + get_version_name=DEFAULT, + get_endless_key_uris=DEFAULT, + get_home_folder=DEFAULT, + ) + @patch.multiple( + "kolibri_android.main_activity.kolibri_bus.KolibriAppProcessBus", + run=DEFAULT, + ) + def test_activity_run(self, **mocks): + mocks["get_signature_key_issuing_organization"].return_value = "test" + mocks["get_timezone_name"].return_value = "UTC" + mocks["get_version_name"].return_value = "Unknown" + mocks["get_endless_key_uris"].return_value = None + mocks["get_home_folder"].return_value = self.kolibri_home_tempdir.name + + from kolibri_android.main_activity.activity import MainActivity + + main_activity = MainActivity() + + mocks["configure_webview"].assert_called_once() + + (on_load_fn, on_load_with_usb_fn, on_loading_ready_fn) = mocks[ + "configure_webview" + ].call_args.args + + on_load_fn() + + # FIXME: We can't use main_activity.run() because it loops forever. + + main_activity.start_kolibri() + + mocks["run"].assert_called_once()