Skip to content

Commit 5c192d7

Browse files
authored
Merge pull request #3358 from ElliotGarbus/kivy3-bootstrap-contract-tests
tests: cover _kivy_bootstrap's contract implementation
2 parents 0280912 + 28e4fcf commit 5c192d7

1 file changed

Lines changed: 136 additions & 0 deletions

File tree

tests/test_kivy_bootstrap.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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

Comments
 (0)