Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions tests/test_bdistapk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import sys
from unittest import mock
from setuptools.dist import Distribution

from pythonforandroid.bdistapk import (
argv_contains,
BdistAPK,
BdistAAR,
BdistAAB,
)


class TestArgvContains:
"""Test argv_contains helper function."""

def test_argv_contains_present(self):
"""Test argv_contains returns True when argument is present."""
with mock.patch.object(sys, 'argv', ['prog', '--name=test', '--version=1.0']):
assert argv_contains('--name')
assert argv_contains('--version')

def test_argv_contains_partial_match(self):
"""Test argv_contains returns True for partial matches."""
with mock.patch.object(sys, 'argv', ['prog', '--name=test']):
assert argv_contains('--name')
assert argv_contains('--nam')

def test_argv_contains_not_present(self):
"""Test argv_contains returns False when argument is not present."""
with mock.patch.object(sys, 'argv', ['prog', '--name=test']):
assert not argv_contains('--package')
assert not argv_contains('--arch')


class TestBdist:
"""Test Bdist base class."""

def setup_method(self):
"""Set up test fixtures."""
self.distribution = Distribution({
'name': 'TestApp',
'version': '1.0.0',
})
self.distribution.package_data = {'testapp': ['*.py', '*.kv']}

@mock.patch('pythonforandroid.bdistapk.ensure_dir')
@mock.patch('pythonforandroid.bdistapk.rmdir')
def test_initialize_options(self, mock_rmdir, mock_ensure_dir):
"""Test initialize_options sets attributes from user_options."""
bdist = BdistAPK(self.distribution)
bdist.user_options = [('name=', None, None), ('version=', None, None)]

bdist.initialize_options()

assert hasattr(bdist, 'name')
assert hasattr(bdist, 'version')

@mock.patch('pythonforandroid.bdistapk.argv_contains')
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
@mock.patch('pythonforandroid.bdistapk.rmdir')
def test_finalize_options_injects_defaults(
self, mock_rmdir, mock_ensure_dir, mock_argv_contains
):
"""Test finalize_options injects default name, package, version, arch."""
mock_argv_contains.return_value = False

with mock.patch.object(sys, 'argv', ['setup.py', 'apk']):
bdist = BdistAPK(self.distribution)
bdist.finalize_options()

# Check that defaults were added to sys.argv
argv_str = ' '.join(sys.argv)
assert '--name=' in argv_str or any('--name' in arg for arg in sys.argv)

@mock.patch('pythonforandroid.bdistapk.argv_contains')
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
@mock.patch('pythonforandroid.bdistapk.rmdir')
def test_finalize_options_permissions_handling(
self, mock_rmdir, mock_ensure_dir, mock_argv_contains
):
"""Test finalize_options handles permissions list correctly."""
mock_argv_contains.side_effect = lambda x: x != '--permissions'

# Set up permissions in the distribution command options
self.distribution.command_options['apk'] = {
'permissions': ('setup.py', ['INTERNET', 'CAMERA'])
}

with mock.patch.object(sys, 'argv', ['setup.py', 'apk']):
bdist = BdistAPK(self.distribution)
bdist.package_type = 'apk'
bdist.finalize_options()

# Check permissions were added
assert any('--permission=INTERNET' in arg for arg in sys.argv)
assert any('--permission=CAMERA' in arg for arg in sys.argv)

@mock.patch('pythonforandroid.entrypoints.main')
@mock.patch('pythonforandroid.bdistapk.argv_contains')
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
@mock.patch('pythonforandroid.bdistapk.rmdir')
@mock.patch('pythonforandroid.bdistapk.copyfile')
@mock.patch('pythonforandroid.bdistapk.glob')
def test_run_calls_main(
self, mock_glob, mock_copyfile, mock_rmdir, mock_ensure_dir,
mock_argv_contains, mock_main
):
"""Test run() calls prepare_build_dir and then main()."""
mock_glob.return_value = ['testapp/main.py']
mock_argv_contains.return_value = False # Not using --launcher or --private

with mock.patch.object(sys, 'argv', ['setup.py', 'apk']):
bdist = BdistAPK(self.distribution)
bdist.arch = 'armeabi-v7a'
bdist.run()

mock_rmdir.assert_called()
mock_ensure_dir.assert_called()
mock_main.assert_called_once()
assert sys.argv[1] == 'apk'

@mock.patch('pythonforandroid.bdistapk.argv_contains')
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
@mock.patch('pythonforandroid.bdistapk.rmdir')
@mock.patch('pythonforandroid.bdistapk.copyfile')
@mock.patch('pythonforandroid.bdistapk.glob')
@mock.patch('builtins.exit', side_effect=SystemExit(1))
def test_prepare_build_dir_no_main_py(
self, mock_exit, mock_glob, mock_copyfile,
mock_rmdir, mock_ensure_dir, mock_argv_contains
):
"""Test prepare_build_dir exits if no main.py found and not using launcher."""
mock_glob.return_value = ['testapp/helper.py']
mock_argv_contains.return_value = False # Not using --launcher

bdist = BdistAPK(self.distribution)
bdist.arch = 'armeabi-v7a'

# Expect SystemExit to be raised
try:
bdist.prepare_build_dir()
assert False, "Expected SystemExit to be raised"
except SystemExit:
pass

mock_exit.assert_called_once_with(1)

@mock.patch('pythonforandroid.bdistapk.argv_contains')
@mock.patch('pythonforandroid.bdistapk.ensure_dir')
@mock.patch('pythonforandroid.bdistapk.rmdir')
@mock.patch('pythonforandroid.bdistapk.copyfile')
@mock.patch('pythonforandroid.bdistapk.glob')
def test_prepare_build_dir_with_main_py(
self, mock_glob, mock_copyfile, mock_rmdir,
mock_ensure_dir, mock_argv_contains
):
"""Test prepare_build_dir succeeds when main.py is found."""
mock_glob.return_value = ['testapp/main.py', 'testapp/helper.py']
# Return False for all argv_contains checks (no --launcher, no --private)
mock_argv_contains.return_value = False

with mock.patch.object(sys, 'argv', ['setup.py', 'apk']):
bdist = BdistAPK(self.distribution)
bdist.arch = 'armeabi-v7a'
bdist.prepare_build_dir()

# Should have copied files (glob might return duplicates)
assert mock_copyfile.call_count >= 2
# Should have added --private argument
assert any('--private=' in arg for arg in sys.argv)


class TestBdistSubclasses:
"""Test BdistAPK, BdistAAR, BdistAAB subclasses."""

def setup_method(self):
"""Set up test fixtures."""
self.distribution = Distribution({
'name': 'TestApp',
'version': '1.0.0',
})
self.distribution.package_data = {}

def test_bdist_apk_package_type(self):
"""Test BdistAPK has correct package_type."""
bdist = BdistAPK(self.distribution)
assert bdist.package_type == 'apk'
assert bdist.description == 'Create an APK with python-for-android'

def test_bdist_aar_package_type(self):
"""Test BdistAAR has correct package_type."""
bdist = BdistAAR(self.distribution)
assert bdist.package_type == 'aar'
assert bdist.description == 'Create an AAR with python-for-android'

def test_bdist_aab_package_type(self):
"""Test BdistAAB has correct package_type."""
bdist = BdistAAB(self.distribution)
assert bdist.package_type == 'aab'
assert bdist.description == 'Create an AAB with python-for-android'
63 changes: 63 additions & 0 deletions tests/test_entrypoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from unittest import mock

from pythonforandroid.entrypoints import main
from pythonforandroid.util import BuildInterruptingException


class TestMain:
"""Test the main entry point function."""

@mock.patch('pythonforandroid.toolchain.ToolchainCL')
@mock.patch('pythonforandroid.entrypoints.check_python_version')
def test_main_success(self, mock_check_version, mock_toolchain):
"""Test main() executes successfully with valid Python version."""
main()

mock_check_version.assert_called_once()
mock_toolchain.assert_called_once()

@mock.patch('pythonforandroid.entrypoints.handle_build_exception')
@mock.patch('pythonforandroid.toolchain.ToolchainCL')
@mock.patch('pythonforandroid.entrypoints.check_python_version')
def test_main_build_interrupting_exception(
self, mock_check_version, mock_toolchain, mock_handler
):
"""Test main() catches BuildInterruptingException and handles it."""
exc = BuildInterruptingException("Build failed", "Try reinstalling")
mock_toolchain.side_effect = exc

main()

mock_check_version.assert_called_once()
mock_toolchain.assert_called_once()
mock_handler.assert_called_once_with(exc)

@mock.patch('pythonforandroid.toolchain.ToolchainCL')
@mock.patch('pythonforandroid.entrypoints.check_python_version')
def test_main_other_exception_propagates(
self, mock_check_version, mock_toolchain
):
"""Test main() allows non-BuildInterruptingException to propagate."""
mock_toolchain.side_effect = RuntimeError("Unexpected error")

try:
main()
assert False, "Expected RuntimeError to be raised"
except RuntimeError as e:
assert str(e) == "Unexpected error"

mock_check_version.assert_called_once()
mock_toolchain.assert_called_once()

@mock.patch('pythonforandroid.entrypoints.check_python_version')
def test_main_python_version_check_fails(self, mock_check_version):
"""Test main() allows Python version check failure to propagate."""
mock_check_version.side_effect = SystemExit(1)

try:
main()
assert False, "Expected SystemExit to be raised"
except SystemExit as e:
assert e.code == 1

mock_check_version.assert_called_once()
67 changes: 67 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,70 @@ def test_max_build_tool_version(self):
result = util.max_build_tool_version(build_tools_versions)

self.assertEqual(result, expected_result)

def test_load_source(self):
"""
Test method :meth:`~pythonforandroid.util.load_source`.
We test loading a Python module from a file path using importlib.
"""
with TemporaryDirectory() as temp_dir:
# Create a test module file
test_module_path = Path(temp_dir) / "test_module.py"
with open(test_module_path, "w") as f:
f.write("TEST_VALUE = 42\n")
f.write("def test_function():\n")
f.write(" return 'hello'\n")

# Load the module
loaded_module = util.load_source("test_module", str(test_module_path))

# Verify the module was loaded correctly
self.assertEqual(loaded_module.TEST_VALUE, 42)
self.assertEqual(loaded_module.test_function(), 'hello')

@mock.patch("pythonforandroid.util.exists")
@mock.patch("shutil.rmtree")
def test_rmdir_exists(self, mock_rmtree, mock_exists):
"""
Test method :meth:`~pythonforandroid.util.rmdir` when directory exists.
We mock exists to return True and verify rmtree is called.
"""
mock_exists.return_value = True
util.rmdir("/fake/directory")
mock_rmtree.assert_called_once_with("/fake/directory", False)

@mock.patch("pythonforandroid.util.exists")
@mock.patch("shutil.rmtree")
def test_rmdir_not_exists(self, mock_rmtree, mock_exists):
"""
Test method :meth:`~pythonforandroid.util.rmdir` when directory doesn't exist.
We mock exists to return False and verify rmtree is not called.
"""
mock_exists.return_value = False
util.rmdir("/fake/directory")
mock_rmtree.assert_not_called()

@mock.patch("pythonforandroid.util.exists")
@mock.patch("shutil.rmtree")
def test_rmdir_ignore_errors(self, mock_rmtree, mock_exists):
"""
Test method :meth:`~pythonforandroid.util.rmdir` with ignore_errors flag.
We verify that the ignore_errors parameter is passed to rmtree.
"""
mock_exists.return_value = True
util.rmdir("/fake/directory", ignore_errors=True)
mock_rmtree.assert_called_once_with("/fake/directory", True)

@mock.patch("pythonforandroid.util.mock")
def test_patch_wheel_setuptools_logging(self, mock_mock):
"""
Test method :meth:`~pythonforandroid.util.patch_wheel_setuptools_logging`.
We verify it returns a mock.patch object for the wheel logging module.
"""
mock_patch_obj = mock.Mock()
mock_mock.patch.return_value = mock_patch_obj

result = util.patch_wheel_setuptools_logging()

mock_mock.patch.assert_called_once_with("wheel._setuptools_logging.configure")
self.assertEqual(result, mock_patch_obj)
Loading