Skip to content

Commit 244b138

Browse files
committed
✅ Tests for entrypoints, bdistapk, and util modules
Add test coverage for three more modules: - entrypoints.py: 4 tests covering main() exception handling (coverage: 33% -> 100%) - bdistapk.py: 12 tests covering setuptools command handlers (coverage: 0% -> 86%) - util.py: 5 tests for load_source(), rmdir(), and patch_wheel_setuptools_logging() (coverage: -> 95%)
1 parent 4536b72 commit 244b138

File tree

3 files changed

+331
-0
lines changed

3 files changed

+331
-0
lines changed

tests/test_bdistapk.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import sys
2+
from unittest import mock
3+
from setuptools.dist import Distribution
4+
5+
from pythonforandroid.bdistapk import (
6+
argv_contains,
7+
Bdist,
8+
BdistAPK,
9+
BdistAAR,
10+
BdistAAB,
11+
)
12+
13+
14+
class TestArgvContains:
15+
"""Test argv_contains helper function."""
16+
17+
def test_argv_contains_present(self):
18+
"""Test argv_contains returns True when argument is present."""
19+
with mock.patch.object(sys, 'argv', ['prog', '--name=test', '--version=1.0']):
20+
assert argv_contains('--name')
21+
assert argv_contains('--version')
22+
23+
def test_argv_contains_partial_match(self):
24+
"""Test argv_contains returns True for partial matches."""
25+
with mock.patch.object(sys, 'argv', ['prog', '--name=test']):
26+
assert argv_contains('--name')
27+
assert argv_contains('--nam')
28+
29+
def test_argv_contains_not_present(self):
30+
"""Test argv_contains returns False when argument is not present."""
31+
with mock.patch.object(sys, 'argv', ['prog', '--name=test']):
32+
assert not argv_contains('--package')
33+
assert not argv_contains('--arch')
34+
35+
36+
class TestBdist:
37+
"""Test Bdist base class."""
38+
39+
def setup_method(self):
40+
"""Set up test fixtures."""
41+
self.distribution = Distribution({
42+
'name': 'TestApp',
43+
'version': '1.0.0',
44+
})
45+
self.distribution.package_data = {'testapp': ['*.py', '*.kv']}
46+
47+
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
48+
@mock.patch('pythonforandroid.bdistapk.rmdir')
49+
def test_initialize_options(self, mock_rmdir, mock_ensure_dir):
50+
"""Test initialize_options sets attributes from user_options."""
51+
bdist = BdistAPK(self.distribution)
52+
bdist.user_options = [('name=', None, None), ('version=', None, None)]
53+
54+
bdist.initialize_options()
55+
56+
assert hasattr(bdist, 'name')
57+
assert hasattr(bdist, 'version')
58+
59+
@mock.patch('pythonforandroid.bdistapk.argv_contains')
60+
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
61+
@mock.patch('pythonforandroid.bdistapk.rmdir')
62+
def test_finalize_options_injects_defaults(
63+
self, mock_rmdir, mock_ensure_dir, mock_argv_contains
64+
):
65+
"""Test finalize_options injects default name, package, version, arch."""
66+
mock_argv_contains.return_value = False
67+
68+
with mock.patch.object(sys, 'argv', ['setup.py', 'apk']):
69+
bdist = BdistAPK(self.distribution)
70+
bdist.finalize_options()
71+
72+
# Check that defaults were added to sys.argv
73+
argv_str = ' '.join(sys.argv)
74+
assert '--name=' in argv_str or any('--name' in arg for arg in sys.argv)
75+
76+
@mock.patch('pythonforandroid.bdistapk.argv_contains')
77+
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
78+
@mock.patch('pythonforandroid.bdistapk.rmdir')
79+
def test_finalize_options_permissions_handling(
80+
self, mock_rmdir, mock_ensure_dir, mock_argv_contains
81+
):
82+
"""Test finalize_options handles permissions list correctly."""
83+
mock_argv_contains.side_effect = lambda x: x != '--permissions'
84+
85+
# Set up permissions in the distribution command options
86+
self.distribution.command_options['apk'] = {
87+
'permissions': ('setup.py', ['INTERNET', 'CAMERA'])
88+
}
89+
90+
with mock.patch.object(sys, 'argv', ['setup.py', 'apk']):
91+
bdist = BdistAPK(self.distribution)
92+
bdist.package_type = 'apk'
93+
bdist.finalize_options()
94+
95+
# Check permissions were added
96+
assert any('--permission=INTERNET' in arg for arg in sys.argv)
97+
assert any('--permission=CAMERA' in arg for arg in sys.argv)
98+
99+
@mock.patch('pythonforandroid.entrypoints.main')
100+
@mock.patch('pythonforandroid.bdistapk.argv_contains')
101+
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
102+
@mock.patch('pythonforandroid.bdistapk.rmdir')
103+
@mock.patch('pythonforandroid.bdistapk.copyfile')
104+
@mock.patch('pythonforandroid.bdistapk.glob')
105+
def test_run_calls_main(
106+
self, mock_glob, mock_copyfile, mock_rmdir, mock_ensure_dir,
107+
mock_argv_contains, mock_main
108+
):
109+
"""Test run() calls prepare_build_dir and then main()."""
110+
mock_glob.return_value = ['testapp/main.py']
111+
mock_argv_contains.return_value = False # Not using --launcher or --private
112+
113+
with mock.patch.object(sys, 'argv', ['setup.py', 'apk']):
114+
bdist = BdistAPK(self.distribution)
115+
bdist.arch = 'armeabi-v7a'
116+
bdist.run()
117+
118+
mock_rmdir.assert_called()
119+
mock_ensure_dir.assert_called()
120+
mock_main.assert_called_once()
121+
assert sys.argv[1] == 'apk'
122+
123+
@mock.patch('pythonforandroid.bdistapk.argv_contains')
124+
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
125+
@mock.patch('pythonforandroid.bdistapk.rmdir')
126+
@mock.patch('pythonforandroid.bdistapk.copyfile')
127+
@mock.patch('pythonforandroid.bdistapk.glob')
128+
@mock.patch('builtins.exit', side_effect=SystemExit(1))
129+
def test_prepare_build_dir_no_main_py(
130+
self, mock_exit, mock_glob, mock_copyfile,
131+
mock_rmdir, mock_ensure_dir, mock_argv_contains
132+
):
133+
"""Test prepare_build_dir exits if no main.py found and not using launcher."""
134+
mock_glob.return_value = ['testapp/helper.py']
135+
mock_argv_contains.return_value = False # Not using --launcher
136+
137+
bdist = BdistAPK(self.distribution)
138+
bdist.arch = 'armeabi-v7a'
139+
140+
# Expect SystemExit to be raised
141+
try:
142+
bdist.prepare_build_dir()
143+
assert False, "Expected SystemExit to be raised"
144+
except SystemExit:
145+
pass
146+
147+
mock_exit.assert_called_once_with(1)
148+
149+
@mock.patch('pythonforandroid.bdistapk.argv_contains')
150+
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
151+
@mock.patch('pythonforandroid.bdistapk.rmdir')
152+
@mock.patch('pythonforandroid.bdistapk.copyfile')
153+
@mock.patch('pythonforandroid.bdistapk.glob')
154+
def test_prepare_build_dir_with_main_py(
155+
self, mock_glob, mock_copyfile, mock_rmdir,
156+
mock_ensure_dir, mock_argv_contains
157+
):
158+
"""Test prepare_build_dir succeeds when main.py is found."""
159+
mock_glob.return_value = ['testapp/main.py', 'testapp/helper.py']
160+
# Return False for all argv_contains checks (no --launcher, no --private)
161+
mock_argv_contains.return_value = False
162+
163+
with mock.patch.object(sys, 'argv', ['setup.py', 'apk']):
164+
bdist = BdistAPK(self.distribution)
165+
bdist.arch = 'armeabi-v7a'
166+
bdist.prepare_build_dir()
167+
168+
# Should have copied files (glob might return duplicates)
169+
assert mock_copyfile.call_count >= 2
170+
# Should have added --private argument
171+
assert any('--private=' in arg for arg in sys.argv)
172+
173+
174+
class TestBdistSubclasses:
175+
"""Test BdistAPK, BdistAAR, BdistAAB subclasses."""
176+
177+
def setup_method(self):
178+
"""Set up test fixtures."""
179+
self.distribution = Distribution({
180+
'name': 'TestApp',
181+
'version': '1.0.0',
182+
})
183+
self.distribution.package_data = {}
184+
185+
def test_bdist_apk_package_type(self):
186+
"""Test BdistAPK has correct package_type."""
187+
bdist = BdistAPK(self.distribution)
188+
assert bdist.package_type == 'apk'
189+
assert bdist.description == 'Create an APK with python-for-android'
190+
191+
def test_bdist_aar_package_type(self):
192+
"""Test BdistAAR has correct package_type."""
193+
bdist = BdistAAR(self.distribution)
194+
assert bdist.package_type == 'aar'
195+
assert bdist.description == 'Create an AAR with python-for-android'
196+
197+
def test_bdist_aab_package_type(self):
198+
"""Test BdistAAB has correct package_type."""
199+
bdist = BdistAAB(self.distribution)
200+
assert bdist.package_type == 'aab'
201+
assert bdist.description == 'Create an AAB with python-for-android'

tests/test_entrypoints.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from unittest import mock
2+
3+
from pythonforandroid.entrypoints import main
4+
from pythonforandroid.util import BuildInterruptingException
5+
6+
7+
class TestMain:
8+
"""Test the main entry point function."""
9+
10+
@mock.patch('pythonforandroid.toolchain.ToolchainCL')
11+
@mock.patch('pythonforandroid.entrypoints.check_python_version')
12+
def test_main_success(self, mock_check_version, mock_toolchain):
13+
"""Test main() executes successfully with valid Python version."""
14+
main()
15+
16+
mock_check_version.assert_called_once()
17+
mock_toolchain.assert_called_once()
18+
19+
@mock.patch('pythonforandroid.entrypoints.handle_build_exception')
20+
@mock.patch('pythonforandroid.toolchain.ToolchainCL')
21+
@mock.patch('pythonforandroid.entrypoints.check_python_version')
22+
def test_main_build_interrupting_exception(
23+
self, mock_check_version, mock_toolchain, mock_handler
24+
):
25+
"""Test main() catches BuildInterruptingException and handles it."""
26+
exc = BuildInterruptingException("Build failed", "Try reinstalling")
27+
mock_toolchain.side_effect = exc
28+
29+
main()
30+
31+
mock_check_version.assert_called_once()
32+
mock_toolchain.assert_called_once()
33+
mock_handler.assert_called_once_with(exc)
34+
35+
@mock.patch('pythonforandroid.toolchain.ToolchainCL')
36+
@mock.patch('pythonforandroid.entrypoints.check_python_version')
37+
def test_main_other_exception_propagates(
38+
self, mock_check_version, mock_toolchain
39+
):
40+
"""Test main() allows non-BuildInterruptingException to propagate."""
41+
mock_toolchain.side_effect = RuntimeError("Unexpected error")
42+
43+
try:
44+
main()
45+
assert False, "Expected RuntimeError to be raised"
46+
except RuntimeError as e:
47+
assert str(e) == "Unexpected error"
48+
49+
mock_check_version.assert_called_once()
50+
mock_toolchain.assert_called_once()
51+
52+
@mock.patch('pythonforandroid.entrypoints.check_python_version')
53+
def test_main_python_version_check_fails(self, mock_check_version):
54+
"""Test main() allows Python version check failure to propagate."""
55+
mock_check_version.side_effect = SystemExit(1)
56+
57+
try:
58+
main()
59+
assert False, "Expected SystemExit to be raised"
60+
except SystemExit as e:
61+
assert e.code == 1
62+
63+
mock_check_version.assert_called_once()

tests/test_util.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,70 @@ def test_max_build_tool_version(self):
230230
result = util.max_build_tool_version(build_tools_versions)
231231

232232
self.assertEqual(result, expected_result)
233+
234+
def test_load_source(self):
235+
"""
236+
Test method :meth:`~pythonforandroid.util.load_source`.
237+
We test loading a Python module from a file path using importlib.
238+
"""
239+
with TemporaryDirectory() as temp_dir:
240+
# Create a test module file
241+
test_module_path = Path(temp_dir) / "test_module.py"
242+
with open(test_module_path, "w") as f:
243+
f.write("TEST_VALUE = 42\n")
244+
f.write("def test_function():\n")
245+
f.write(" return 'hello'\n")
246+
247+
# Load the module
248+
loaded_module = util.load_source("test_module", str(test_module_path))
249+
250+
# Verify the module was loaded correctly
251+
self.assertEqual(loaded_module.TEST_VALUE, 42)
252+
self.assertEqual(loaded_module.test_function(), 'hello')
253+
254+
@mock.patch("pythonforandroid.util.exists")
255+
@mock.patch("shutil.rmtree")
256+
def test_rmdir_exists(self, mock_rmtree, mock_exists):
257+
"""
258+
Test method :meth:`~pythonforandroid.util.rmdir` when directory exists.
259+
We mock exists to return True and verify rmtree is called.
260+
"""
261+
mock_exists.return_value = True
262+
util.rmdir("/fake/directory")
263+
mock_rmtree.assert_called_once_with("/fake/directory", False)
264+
265+
@mock.patch("pythonforandroid.util.exists")
266+
@mock.patch("shutil.rmtree")
267+
def test_rmdir_not_exists(self, mock_rmtree, mock_exists):
268+
"""
269+
Test method :meth:`~pythonforandroid.util.rmdir` when directory doesn't exist.
270+
We mock exists to return False and verify rmtree is not called.
271+
"""
272+
mock_exists.return_value = False
273+
util.rmdir("/fake/directory")
274+
mock_rmtree.assert_not_called()
275+
276+
@mock.patch("pythonforandroid.util.exists")
277+
@mock.patch("shutil.rmtree")
278+
def test_rmdir_ignore_errors(self, mock_rmtree, mock_exists):
279+
"""
280+
Test method :meth:`~pythonforandroid.util.rmdir` with ignore_errors flag.
281+
We verify that the ignore_errors parameter is passed to rmtree.
282+
"""
283+
mock_exists.return_value = True
284+
util.rmdir("/fake/directory", ignore_errors=True)
285+
mock_rmtree.assert_called_once_with("/fake/directory", True)
286+
287+
@mock.patch("pythonforandroid.util.mock")
288+
def test_patch_wheel_setuptools_logging(self, mock_mock):
289+
"""
290+
Test method :meth:`~pythonforandroid.util.patch_wheel_setuptools_logging`.
291+
We verify it returns a mock.patch object for the wheel logging module.
292+
"""
293+
mock_patch_obj = mock.Mock()
294+
mock_mock.patch.return_value = mock_patch_obj
295+
296+
result = util.patch_wheel_setuptools_logging()
297+
298+
mock_mock.patch.assert_called_once_with("wheel._setuptools_logging.configure")
299+
self.assertEqual(result, mock_patch_obj)

0 commit comments

Comments
 (0)