Skip to content

Commit 0155c7e

Browse files
authored
Merge pull request #3254 from AndreMiras/feature/increase_test_coverage_entrypoint_bdistapk
✅ Tests for entrypoints, bdistapk, and util modules
2 parents 56869f2 + 17ca5f2 commit 0155c7e

File tree

3 files changed

+330
-0
lines changed

3 files changed

+330
-0
lines changed

tests/test_bdistapk.py

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