|
| 1 | +"""Tests for pythonforandroid's implementation of Kivy's Android bootstrap |
| 2 | +contract (``_kivy_bootstrap.py``, shipped by the ``android`` recipe). |
| 3 | +
|
| 4 | +Off-device, so ``jnius`` and the build-generated ``android.config`` are faked, |
| 5 | +following the same approach as test_androidmodule_ctypes_finder.py. Faking |
| 6 | +``android.config`` directly in ``sys.modules`` (rather than importing the real |
| 7 | +``android`` package) also sidesteps ``android/__init__.py``'s |
| 8 | +``from android._android import *``, which needs the native extension this |
| 9 | +module must not depend on. |
| 10 | +""" |
| 11 | +import os |
| 12 | +import sys |
| 13 | +from contextlib import contextmanager |
| 14 | +from types import ModuleType, SimpleNamespace |
| 15 | +from unittest import mock |
| 16 | +from unittest.mock import MagicMock |
| 17 | + |
| 18 | +import pytest |
| 19 | + |
| 20 | +# Import the module under test the way Kivy does: top-level, not under |
| 21 | +# `android` (see the comment in recipes/android/src/setup.py). |
| 22 | +android_src_folder = os.path.abspath(os.path.join( |
| 23 | + os.path.dirname(__file__), |
| 24 | + "..", "pythonforandroid", "recipes", "android", "src" |
| 25 | +)) |
| 26 | +sys.path.insert(0, android_src_folder) |
| 27 | + |
| 28 | + |
| 29 | +@contextmanager |
| 30 | +def _bootstrap_module(activity_class_name="org.kivy.android.PythonActivity"): |
| 31 | + """Import a fresh ``_kivy_bootstrap`` against faked jnius/android.config. |
| 32 | +
|
| 33 | + A fresh import per use avoids the module's cached ``_activity_class`` |
| 34 | + leaking between tests. |
| 35 | + """ |
| 36 | + fake_jnius = MagicMock() |
| 37 | + fake_config = ModuleType("android.config") |
| 38 | + fake_config.ACTIVITY_CLASS_NAME = activity_class_name |
| 39 | + with mock.patch.dict( |
| 40 | + sys.modules, jnius=fake_jnius, **{"android.config": fake_config} |
| 41 | + ): |
| 42 | + sys.modules.pop("_kivy_bootstrap", None) |
| 43 | + import _kivy_bootstrap |
| 44 | + try: |
| 45 | + yield SimpleNamespace(module=_kivy_bootstrap, jnius=fake_jnius) |
| 46 | + finally: |
| 47 | + sys.modules.pop("_kivy_bootstrap", None) |
| 48 | + |
| 49 | + |
| 50 | +@pytest.fixture |
| 51 | +def kivy_bootstrap(): |
| 52 | + with _bootstrap_module() as bootstrap: |
| 53 | + yield bootstrap |
| 54 | + |
| 55 | + |
| 56 | +def test_get_activity_reflects_the_configured_class(kivy_bootstrap): |
| 57 | + """The class reflected is the build's ACTIVITY_CLASS_NAME, not a literal.""" |
| 58 | + activity = object() |
| 59 | + java_class = MagicMock(mActivity=activity) |
| 60 | + # `_kivy_bootstrap` did `from jnius import autoclass` at import time, so |
| 61 | + # the name it calls is already bound to this mock: configure its return |
| 62 | + # value rather than replacing the attribute on `kivy_bootstrap.jnius`, |
| 63 | + # which the module would never see. |
| 64 | + kivy_bootstrap.jnius.autoclass.return_value = java_class |
| 65 | + |
| 66 | + assert kivy_bootstrap.module.get_activity() is activity |
| 67 | + kivy_bootstrap.jnius.autoclass.assert_called_once_with( |
| 68 | + "org.kivy.android.PythonActivity" |
| 69 | + ) |
| 70 | + |
| 71 | + |
| 72 | +def test_get_activity_honours_custom_activity_class_name(): |
| 73 | + """--activity-class-name is honoured: the name comes from android.config.""" |
| 74 | + with _bootstrap_module("com.example.CustomActivity") as bootstrap: |
| 75 | + bootstrap.module.get_activity() |
| 76 | + bootstrap.jnius.autoclass.assert_called_once_with( |
| 77 | + "com.example.CustomActivity" |
| 78 | + ) |
| 79 | + |
| 80 | + |
| 81 | +def test_get_activity_may_return_none(kivy_bootstrap): |
| 82 | + """A p4a service has no Activity; None is a legitimate answer.""" |
| 83 | + java_class = MagicMock(mActivity=None) |
| 84 | + kivy_bootstrap.jnius.autoclass.return_value = java_class |
| 85 | + |
| 86 | + assert kivy_bootstrap.module.get_activity() is None |
| 87 | + |
| 88 | + |
| 89 | +def test_get_activity_resolves_the_class_once_but_reads_it_live(kivy_bootstrap): |
| 90 | + """The reflected class is cached; the live Activity is read fresh each call. |
| 91 | +
|
| 92 | + Regression guard for the module's core promise: Android may recreate the |
| 93 | + Activity (rotation, config change, process death), so a second call must |
| 94 | + return whatever ``mActivity`` is *now*, not a value from the first call. |
| 95 | + """ |
| 96 | + java_class = MagicMock() |
| 97 | + kivy_bootstrap.jnius.autoclass.return_value = java_class |
| 98 | + |
| 99 | + first_activity = object() |
| 100 | + java_class.mActivity = first_activity |
| 101 | + assert kivy_bootstrap.module.get_activity() is first_activity |
| 102 | + |
| 103 | + second_activity = object() |
| 104 | + java_class.mActivity = second_activity |
| 105 | + assert kivy_bootstrap.module.get_activity() is second_activity |
| 106 | + |
| 107 | + # autoclass() itself -- the expensive reflection -- only happens once. |
| 108 | + kivy_bootstrap.jnius.autoclass.assert_called_once() |
| 109 | + |
| 110 | + |
| 111 | +def test_remove_presplash_is_a_noop_without_an_activity(kivy_bootstrap): |
| 112 | + """No Activity (a service) means no splash to remove, and no error.""" |
| 113 | + java_class = MagicMock(mActivity=None) |
| 114 | + kivy_bootstrap.jnius.autoclass.return_value = java_class |
| 115 | + |
| 116 | + kivy_bootstrap.module.remove_presplash() # must not raise |
| 117 | + |
| 118 | + |
| 119 | +def test_remove_presplash_calls_removeLoadingScreen(kivy_bootstrap): |
| 120 | + """The splash is dismissed by asking the Activity, not a fixed method map.""" |
| 121 | + activity = MagicMock() |
| 122 | + java_class = MagicMock(mActivity=activity) |
| 123 | + kivy_bootstrap.jnius.autoclass.return_value = java_class |
| 124 | + |
| 125 | + kivy_bootstrap.module.remove_presplash() |
| 126 | + |
| 127 | + activity.removeLoadingScreen.assert_called_once_with() |
| 128 | + |
| 129 | + |
| 130 | +def test_remove_presplash_is_a_noop_when_activity_lacks_the_method(kivy_bootstrap): |
| 131 | + """service_only (and any custom activity) may have no splash to remove.""" |
| 132 | + activity = SimpleNamespace() # no removeLoadingScreen attribute at all |
| 133 | + java_class = MagicMock(mActivity=activity) |
| 134 | + kivy_bootstrap.jnius.autoclass.return_value = java_class |
| 135 | + |
| 136 | + kivy_bootstrap.module.remove_presplash() # must not raise |
0 commit comments